/** * 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( "resource://testing-common/Sinon.sys.mjs" ); 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 = [ { url: "https://www.google.com/search?q=firefox+history", title: "Google Search: firefox history", visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago }, { url: "https://developer.mozilla.org/en-US/docs/Web/JavaScript", title: "JavaScript | MDN", visits: [{ date: new Date(now - 10 * 60 * 1000) }], // 10 min ago }, { url: "https://news.ycombinator.com/", title: "Hacker News", visits: [{ date: new Date(now - 15 * 60 * 1000) }], }, { url: "https://search.brave.com/search?q=mozsqlite", title: "Brave Search: mozsqlite", visits: [{ date: new Date(now - 20 * 60 * 1000) }], }, { url: "https://mozilla.org/en-US/", 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 = { url: "https://example.com/older", title: "Older Page", visits: [{ date: new Date(now - 60 * 60 * 1000) }], // 60 min ago }; const recent = { url: "https://example.com/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 = { url: "https://example.com/older", title: "Older Page", visits: [{ date: new Date(now - 60 * 60 * 1000) }], // 60 min ago }; const recent = { url: "https://example.com/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 = { url: "https://example.com/before-window", title: "Before Window", visits: [{ date: new Date(now - 3 * 60 * 60 * 1000) }], // 3h ago }; const inWindow = { url: "https://example.com/in-window", title: "In Window", visits: [{ date: new Date(now - 30 * 60 * 1000) }], // 30 min ago }; const afterWindow = { url: "https://example.com/after-window", 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 = [ { url: "https://www.mozilla.org/en-US/", title: "Internet for people, not profit — Mozilla", visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago }, { url: "https://example.com/other", 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); });