Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/**
* 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
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
const { searchBrowsingHistory } = ChromeUtils.importESModule(
"moz-src:///browser/components/aiwindow/models/SearchBrowsingHistory.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
);
let sb;
// setup
add_task(async function setup() {
sb = sinon.createSandbox();
registerCleanupFunction(() => {
sb.restore();
Services.prefs.clearUserPref("browser.ml.enable");
Services.prefs.clearUserPref("places.semanticHistory.featureGate");
Services.prefs.clearUserPref("browser.search.region");
});
Services.prefs.setBoolPref("browser.ml.enable", true);
Services.prefs.setBoolPref("places.semanticHistory.featureGate", true);
Services.prefs.setCharPref("browser.search.region", "US");
await PlacesUtils.history.clear();
});
// test: empty searchTerm, no time window
add_task(async function test_basic_history_fetch_and_shape() {
await PlacesUtils.history.clear();
const now = Date.now();
const seeded = [
{
title: "Google Search: firefox history",
visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago
},
{
title: "JavaScript | MDN",
visits: [{ date: new Date(now - 10 * 60 * 1000) }], // 10 min ago
},
{
title: "Hacker News",
visits: [{ date: new Date(now - 15 * 60 * 1000) }],
},
{
title: "Brave Search: mozsqlite",
visits: [{ date: new Date(now - 20 * 60 * 1000) }],
},
{
title: "Internet for people, not profit — Mozilla",
visits: [{ date: new Date(now - 25 * 60 * 1000) }],
},
];
await PlacesUtils.history.insertMany(seeded);
const allRowsStr = await searchBrowsingHistory({
searchTerm: "",
startTs: null,
endTs: null,
historyLimit: 15,
});
const allRowsObj = JSON.parse(allRowsStr);
// check count match
Assert.equal(
allRowsObj.count,
seeded.length,
"Should return all seeded records"
);
// check all url match
const urls = allRowsObj.results.map(r => r.url).sort();
const expectedUrls = seeded.map(s => s.url).sort();
Assert.deepEqual(urls, expectedUrls, "Should return all seeded URLs");
// check title and url match
const byUrl = new Map(allRowsObj.results.map(r => [r.url, r]));
for (const { url, title } of seeded) {
Assert.ok(byUrl.has(url), `Has entry for ${url}`);
Assert.equal(byUrl.get(url).title, title, `Title matches for ${url}`);
}
// check visitDate iso string
for (const r of allRowsObj.results) {
Assert.ok(
!isNaN(Date.parse(r.visitDate)),
"visitDate is a valid ISO timestamp"
);
}
});
// test: startTs only
add_task(async function test_time_range_only_startTs() {
await PlacesUtils.history.clear();
const now = Date.now();
const older = {
title: "Older Page",
visits: [{ date: new Date(now - 60 * 60 * 1000) }], // 60 min ago
};
const recent = {
title: "Recent Page",
visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago
};
await PlacesUtils.history.insertMany([older, recent]);
// records after last 10 minutes
const startTs = new Date(now - 10 * 60 * 1000).toISOString(); // ISO input
const rowsStr = await searchBrowsingHistory({
searchTerm: "",
startTs,
endTs: null,
historyLimit: 15,
});
const rows = JSON.parse(rowsStr);
const urls = rows.results.map(r => r.url);
Assert.ok(
urls.includes(recent.url),
"Recent entry should be included when only startTs is set"
);
Assert.ok(
!urls.includes(older.url),
"Older entry should be excluded when only startTs is set"
);
});
// test: endTs only
add_task(async function test_time_range_only_endTs() {
await PlacesUtils.history.clear();
const now = Date.now();
const older = {
title: "Older Page",
visits: [{ date: new Date(now - 60 * 60 * 1000) }], // 60 min ago
};
const recent = {
title: "Recent Page",
visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago
};
await PlacesUtils.history.insertMany([older, recent]);
// Anything before last 10 minutes
const endTs = new Date(now - 10 * 60 * 1000).toISOString(); // ISO input
const rowsStr = await searchBrowsingHistory({
searchTerm: "",
startTs: null,
endTs,
historyLimit: 15,
});
const rows = JSON.parse(rowsStr);
const urls = rows.results.map(r => r.url);
Assert.ok(
urls.includes(older.url),
"Older entry should be included when only endTs is set"
);
Assert.ok(
!urls.includes(recent.url),
"Recent entry should be excluded when only endTs is set"
);
});
// test: startTs + endTs
add_task(async function test_time_range_start_and_endTs() {
await PlacesUtils.history.clear();
const now = Date.now();
const beforeWindow = {
title: "Before Window",
visits: [{ date: new Date(now - 3 * 60 * 60 * 1000) }], // 3h ago
};
const inWindow = {
title: "In Window",
visits: [{ date: new Date(now - 30 * 60 * 1000) }], // 30 min ago
};
const afterWindow = {
title: "After Window",
visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago
};
await PlacesUtils.history.insertMany([beforeWindow, inWindow, afterWindow]);
// Time window: [45min ago, 15min ago]
const startTs = new Date(now - 45 * 60 * 1000).toISOString();
const endTs = new Date(now - 15 * 60 * 1000).toISOString();
const rowsStr = await searchBrowsingHistory({
searchTerm: "",
startTs,
endTs,
historyLimit: 15,
});
const rows = JSON.parse(rowsStr);
const urls = rows.results.map(r => r.url);
Assert.ok(urls.includes(inWindow.url), "In window entry should be included");
Assert.ok(
!urls.includes(beforeWindow.url),
"Before window entry should be excluded"
);
Assert.ok(
!urls.includes(afterWindow.url),
"After window entry should be excluded"
);
});
/**
* Test no results behavior: empty history with and without searchTerm.
*
* We don't try to force the semantic here (that would require a
* running ML engine). Instead we just assert the wrapper's messaging
* when there are no rows.
*/
add_task(async function test_no_results_messages() {
await PlacesUtils.history.clear();
// No search term: time range message.
let outputStr = await searchBrowsingHistory({
searchTerm: "",
startTs: null,
endTs: null,
historyLimit: 15,
});
let output = JSON.parse(outputStr);
Assert.equal(output.results.length, 0, "No results when history is empty");
Assert.ok(
output.message.includes("requested time range"),
"Message explains empty time-range search"
);
// With search term: search specific message.
outputStr = await searchBrowsingHistory({
searchTerm: "mozilla",
startTs: null,
endTs: null,
historyLimit: 15,
});
output = JSON.parse(outputStr);
Assert.equal(output.results.length, 0, "No results for semantic search");
Assert.ok(
output.message.includes("mozilla"),
"Message mentions the search term when there are no matches"
);
});
// test: non-empty searchTerm falls back to basic history search
// when semantic search is disabled via prefs.
add_task(async function test_basic_text_search_when_semantic_disabled() {
await PlacesUtils.history.clear();
const now = Date.now();
const seeded = [
{
title: "Internet for people, not profit — Mozilla",
visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago
},
{
title: "Some Other Site",
visits: [{ date: new Date(now - 10 * 60 * 1000) }], // 10 min ago
},
];
await PlacesUtils.history.insertMany(seeded);
// Disable semantic search so searchBrowsingHistory must fall back
// to the basic history search.
Services.prefs.setBoolPref("browser.ml.enable", false);
Services.prefs.setBoolPref("places.semanticHistory.featureGate", false);
const outputStr = await searchBrowsingHistory({
searchTerm: "mozilla",
startTs: null,
endTs: null,
historyLimit: 15,
});
const output = JSON.parse(outputStr);
Assert.equal(output.searchTerm, "mozilla", "searchTerm match");
Assert.equal(output.results.length, 1, "One history entry is returned");
const urls = output.results.map(r => r.url);
Assert.ok(
urls.includes("https://www.mozilla.org/en-US/"),
"Basic history search should find the Mozilla entry"
);
// Restore prefs
Services.prefs.setBoolPref("browser.ml.enable", true);
Services.prefs.setBoolPref("places.semanticHistory.featureGate", true);
});