Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test runs only with pattern: os != 'android'
- Manifest: browser/components/urlbar/tests/quicksuggest/unit/xpcshell.toml
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
// Tests sports suggestions and related code.
"use strict";
ChromeUtils.defineESModuleGetters(this, {
SportsSuggestions:
"moz-src:///browser/components/urlbar/private/SportsSuggestions.sys.mjs",
});
// 2025-11-01 - game status is "scheduled", without icon
const SUGGESTION_VALUE_SCHEDULED = {
sport: "Sport 3",
query: "query 3",
date: "2025-11-01T17:00:00Z",
home_team: {
name: "Team 3 Home",
score: null,
},
away_team: {
name: "Team 3 Away",
score: null,
},
status_type: "scheduled",
};
add_setup(async function init() {
await Services.search.init();
// Disable search suggestions so we don't hit the network.
Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
// This test deals with `Intl` formating of dates and times, which depends on
// the system locale, and assumes it's en-US. Make sure it's actually en-US.
await QuickSuggestTestUtils.setRegionAndLocale({
locale: "en-US",
skipSuggestReset: true,
});
await QuickSuggestTestUtils.ensureQuickSuggestInit({
merinoSuggestions: merinoSuggestions([SUGGESTION_VALUE_SCHEDULED]),
prefs: [
["sports.featureGate", true],
["suggest.sports", true],
["suggest.quicksuggest.all", true],
],
});
});
add_task(async function telemetryType() {
Assert.equal(
QuickSuggest.getFeature("SportsSuggestions").getSuggestionTelemetryType({}),
"sports",
"Telemetry type should be as expected"
);
});
// The suggestions should be disabled when the relevant prefs are false.
add_task(async function disabledPrefs() {
setNow("2025-10-31T14:00:00-04:00[-04:00]");
let prefs = [
"quicksuggest.enabled",
"sports.featureGate",
"suggest.sports",
"suggest.quicksuggest.all",
];
for (let pref of prefs) {
info("Testing pref: " + pref);
// First make sure the suggestion is added.
await check_results({
context: createContext("test", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
matches: [
expectedResult([
{
query: "query 3",
sport: "Sport 3",
status_type: "scheduled",
date: "2025-11-01T17:00:00Z",
home_team: {
name: "Team 3 Home",
score: null,
},
away_team: {
name: "Team 3 Away",
score: null,
},
},
]),
],
});
// Now disable them.
UrlbarPrefs.set(pref, false);
await check_results({
context: createContext("test", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
matches: [],
});
// Revert.
UrlbarPrefs.set(pref, true);
await QuickSuggestTestUtils.forceSync();
}
});
// Main test for `SportsSuggestions._parseDate`.
add_task(async function datesAndTimes() {
// For each test, we'll set `now`, call `_parseDate` with `date`, and check
// the return value against `expected`.
let tests = [
// date is before this year
{
now: "2025-10-31T12:00:00-07:00[-07:00]",
date: "2013-05-11T04:00:00-07:00",
expected: {
daysUntil: -Infinity,
isFuture: false,
},
},
// date is before yesterday
{
now: [
"2025-10-31T00:00:00-07:00[-07:00]",
"2025-10-31T23:59:59-07:00[-07:00]",
],
date: ["2025-10-29T00:00:00-07:00", "2025-10-29T23:59:59-07:00"],
expected: {
daysUntil: -Infinity,
isFuture: false,
},
},
// date is yesterday
{
now: [
"2025-10-31T00:00:00-07:00[-07:00]",
"2025-10-31T23:59:59-07:00[-07:00]",
],
date: ["2025-10-30T00:00:00-07:00", "2025-10-30T23:59:59-07:00"],
expected: {
daysUntil: -1,
isFuture: false,
},
},
// date is today (past)
{
now: [
"2025-10-31T12:00:00-07:00[-07:00]",
"2025-10-31T23:59:59-07:00[-07:00]",
],
date: ["2025-10-31T00:00:00-07:00", "2025-10-31T11:59:59-07:00"],
expected: {
daysUntil: 0,
isFuture: false,
},
},
// date is today (now)
{
now: "2025-10-31T12:00:00-07:00[-07:00]",
date: "2025-10-31T12:00:00-07:00",
expected: {
daysUntil: 0,
isFuture: false,
},
},
// date is today (future)
{
now: [
"2025-10-31T00:00:00-07:00[-07:00]",
"2025-10-31T12:00:00-07:00[-07:00]",
],
date: ["2025-10-31T12:00:01-07:00", "2025-10-31T23:59:59-07:00"],
expected: {
daysUntil: 0,
isFuture: true,
},
},
// date is tomorrow
{
now: [
"2025-10-31T00:00:00-07:00[-07:00]",
"2025-10-31T23:59:59-07:00[-07:00]",
],
date: ["2025-11-01T00:00:00-07:00", "2025-11-01T23:59:59-07:00"],
expected: {
daysUntil: 1,
isFuture: true,
},
},
// date is after tomorrow
{
now: [
"2025-10-31T00:00:00-07:00[-07:00]",
"2025-10-31T23:59:59-07:00[-07:00]",
],
date: ["2025-11-02T00:00:00-07:00", "2025-11-02T23:59:59-07:00"],
expected: {
daysUntil: Infinity,
isFuture: true,
},
},
// date is after this year
{
now: "2025-10-31T00:00:00-07:00[-07:00]",
date: "3013-05-11T04:00:00-07:00",
expected: {
daysUntil: Infinity,
isFuture: true,
},
},
];
for (let { now, date, expected } of tests) {
let nows = typeof now == "string" ? [now] : now;
let dates = typeof date == "string" ? [date] : date;
for (let n of nows) {
let zonedNow = setNow(n);
for (let d of dates) {
Assert.deepEqual(
SportsSuggestions._parseDate(new Date(d)),
{
...expected,
zonedNow,
zonedDate: new Date(d)
.toTemporalInstant()
.toZonedDateTimeISO(zonedNow),
},
"datesAndTimes test: " + JSON.stringify({ now: n, date: d })
);
}
}
}
});
// Tests `SportsSuggestions._parseDate` with dates across time zone changes.
add_task(function timeZoneTransition() {
// This task is based around 2025-11-02, when Daylight Saving Time ends in the
// U.S. On 2025-11-02 at 2:00 am, the time changes to 1:00 am Standard Time.
let tests = [
// `now` and `date` both in PDT (daylight saving)
{
now: "2025-10-02T12:00:00-07:00[America/Los_Angeles]",
date: "2025-10-01T00:00:00-07:00",
expected: {
daysUntil: -1,
isFuture: false,
},
},
// `now` in PST, `date` in PDT
{
now: "2025-11-03T00:00:00-08:00[America/Los_Angeles]",
date: "2025-11-01T00:00:00-07:00",
expected: {
daysUntil: -Infinity,
isFuture: false,
},
},
{
now: "2025-11-02T12:00:00-08:00[America/Los_Angeles]",
date: "2025-11-01T00:00:00-07:00",
expected: {
daysUntil: -1,
isFuture: false,
},
},
{
now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
date: "2025-11-01T00:00:00-07:00",
expected: {
daysUntil: -1,
isFuture: false,
},
},
{
now: "2025-11-02T23:59:59-08:00[America/Los_Angeles]",
date: "2025-11-01T00:00:00-07:00",
expected: {
daysUntil: -1,
isFuture: false,
},
},
{
now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
date: "2025-11-02T00:00:00-07:00",
expected: {
daysUntil: 0,
isFuture: false,
},
},
{
now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
date: "2025-11-02T01:00:00-07:00",
expected: {
daysUntil: 0,
isFuture: false,
},
},
// `now` in PDT, `date` in PST
{
now: "2025-11-02T01:00:00-07:00[America/Los_Angeles]",
date: "2025-11-02T01:00:00-08:00",
expected: {
daysUntil: 0,
isFuture: true,
},
},
{
now: "2025-11-02T00:00:00-07:00[America/Los_Angeles]",
date: "2025-11-02T01:00:00-08:00",
expected: {
daysUntil: 0,
isFuture: true,
},
},
{
now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
date: "2025-11-02T23:59:59-08:00",
expected: {
daysUntil: 1,
isFuture: true,
},
},
{
now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
date: "2025-11-02T01:00:00-08:00",
expected: {
daysUntil: 1,
isFuture: true,
},
},
{
now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
date: "2025-11-02T12:00:00-08:00",
expected: {
daysUntil: 1,
isFuture: true,
},
},
{
now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
date: "2025-11-03T00:00:00-08:00",
expected: {
daysUntil: Infinity,
isFuture: true,
},
},
// `now` and `date` both in PST (standard time)
{
now: "2025-11-11T12:00:00-08:00[America/Los_Angeles]",
date: "2025-11-10T00:00:00-08:00",
expected: {
daysUntil: -1,
isFuture: false,
},
},
];
for (let { now, date, expected } of tests) {
let zonedNow = setNow(now);
Assert.deepEqual(
SportsSuggestions._parseDate(new Date(date)),
{
...expected,
zonedNow,
zonedDate: new Date(date)
.toTemporalInstant()
.toZonedDateTimeISO(zonedNow),
},
"timeZoneTransition test: " + JSON.stringify({ now, date })
);
}
});
add_task(async function command_notInterested() {
setNow("2025-10-31T14:00:00-04:00[-04:00]");
await doDismissAllTest({
result: expectedResult([
{
query: "query 3",
sport: "Sport 3",
status_type: "scheduled",
date: "2025-11-01T17:00:00Z",
home_team: {
name: "Team 3 Home",
score: null,
},
away_team: {
name: "Team 3 Away",
score: null,
},
},
]),
command: "not_interested",
feature: QuickSuggest.getFeature("SportsSuggestions"),
pref: "suggest.sports",
queries: [{ query: "test" }],
});
});
add_task(async function command_showLessFrequently() {
setNow("2025-10-31T14:00:00-04:00[-04:00]");
UrlbarPrefs.clear("sports.showLessFrequentlyCount");
UrlbarPrefs.clear("sports.minKeywordLength");
let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
realtimeMinKeywordLength: 0,
realtimeShowLessFrequentlyCap: 3,
});
let result = expectedResult([
{
query: "query 3",
sport: "Sport 3",
status_type: "scheduled",
date: "2025-11-01T17:00:00Z",
home_team: {
name: "Team 3 Home",
score: null,
},
away_team: {
name: "Team 3 Away",
score: null,
},
},
]);
const testData = [
{
input: "spo",
before: {
canShowLessFrequently: true,
showLessFrequentlyCount: 0,
minKeywordLength: 0,
},
after: {
canShowLessFrequently: true,
showLessFrequentlyCount: 1,
minKeywordLength: 4,
},
},
{
input: "sport",
before: {
canShowLessFrequently: true,
showLessFrequentlyCount: 1,
minKeywordLength: 4,
},
after: {
canShowLessFrequently: true,
showLessFrequentlyCount: 2,
minKeywordLength: 6,
},
},
{
input: "sports",
before: {
canShowLessFrequently: true,
showLessFrequentlyCount: 2,
minKeywordLength: 6,
},
after: {
canShowLessFrequently: false,
showLessFrequentlyCount: 3,
minKeywordLength: 7,
},
},
];
for (let { input, before, after } of testData) {
let feature = QuickSuggest.getFeature("SportsSuggestions");
await check_results({
context: createContext(input, {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
matches: [result],
});
Assert.equal(
UrlbarPrefs.get("sports.minKeywordLength"),
before.minKeywordLength
);
Assert.equal(feature.canShowLessFrequently, before.canShowLessFrequently);
Assert.equal(
feature.showLessFrequentlyCount,
before.showLessFrequentlyCount
);
triggerCommand({
result,
feature,
command: "show_less_frequently",
searchString: input,
});
Assert.equal(
UrlbarPrefs.get("sports.minKeywordLength"),
after.minKeywordLength
);
Assert.equal(feature.canShowLessFrequently, after.canShowLessFrequently);
Assert.equal(
feature.showLessFrequentlyCount,
after.showLessFrequentlyCount
);
await check_results({
context: createContext(input, {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
matches: [],
});
}
await cleanUpNimbus();
UrlbarPrefs.clear("sports.showLessFrequentlyCount");
UrlbarPrefs.clear("sports.minKeywordLength");
});
let gSandbox;
let gDateStub;
function setNow(dateStr) {
if (!dateStr) {
gSandbox?.restore();
return null;
}
let global = Cu.getGlobalForObject(SportsSuggestions);
if (!gSandbox) {
gSandbox = sinon.createSandbox();
gDateStub = gSandbox.stub(SportsSuggestions, "_zonedDateTimeISO");
}
let zonedNow = global.Temporal.ZonedDateTime.from(dateStr);
gDateStub.returns(zonedNow);
return zonedNow;
}
function merinoSuggestions(values) {
return [
{
provider: "sports",
is_sponsored: false,
score: 0.2,
title: "",
custom_details: {
sports: {
values,
},
},
},
];
}
function expectedResult(expectedItems) {
return {
type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
isBestMatch: true,
hideRowLabel: true,
rowIndex: -1,
heuristic: false,
exposureTelemetry: 0,
payload: {
items: expectedItems,
source: "merino",
provider: "sports",
telemetryType: "sports",
isSponsored: false,
engine: Services.search.defaultEngine.name,
dynamicType: "realtime-sports",
},
};
}