test_SearchBrowsingHistory.js (9338B)
1 /** 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 */ 6 7 const { searchBrowsingHistory } = ChromeUtils.importESModule( 8 "moz-src:///browser/components/aiwindow/models/SearchBrowsingHistory.sys.mjs" 9 ); 10 11 const { sinon } = ChromeUtils.importESModule( 12 "resource://testing-common/Sinon.sys.mjs" 13 ); 14 15 let sb; 16 17 // setup 18 add_task(async function setup() { 19 sb = sinon.createSandbox(); 20 registerCleanupFunction(() => { 21 sb.restore(); 22 Services.prefs.clearUserPref("browser.ml.enable"); 23 Services.prefs.clearUserPref("places.semanticHistory.featureGate"); 24 Services.prefs.clearUserPref("browser.search.region"); 25 }); 26 27 Services.prefs.setBoolPref("browser.ml.enable", true); 28 Services.prefs.setBoolPref("places.semanticHistory.featureGate", true); 29 Services.prefs.setCharPref("browser.search.region", "US"); 30 31 await PlacesUtils.history.clear(); 32 }); 33 34 // test: empty searchTerm, no time window 35 add_task(async function test_basic_history_fetch_and_shape() { 36 await PlacesUtils.history.clear(); 37 38 const now = Date.now(); 39 40 const seeded = [ 41 { 42 url: "https://www.google.com/search?q=firefox+history", 43 title: "Google Search: firefox history", 44 visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago 45 }, 46 { 47 url: "https://developer.mozilla.org/en-US/docs/Web/JavaScript", 48 title: "JavaScript | MDN", 49 visits: [{ date: new Date(now - 10 * 60 * 1000) }], // 10 min ago 50 }, 51 { 52 url: "https://news.ycombinator.com/", 53 title: "Hacker News", 54 visits: [{ date: new Date(now - 15 * 60 * 1000) }], 55 }, 56 { 57 url: "https://search.brave.com/search?q=mozsqlite", 58 title: "Brave Search: mozsqlite", 59 visits: [{ date: new Date(now - 20 * 60 * 1000) }], 60 }, 61 { 62 url: "https://mozilla.org/en-US/", 63 title: "Internet for people, not profit — Mozilla", 64 visits: [{ date: new Date(now - 25 * 60 * 1000) }], 65 }, 66 ]; 67 68 await PlacesUtils.history.insertMany(seeded); 69 70 const allRowsStr = await searchBrowsingHistory({ 71 searchTerm: "", 72 startTs: null, 73 endTs: null, 74 historyLimit: 15, 75 }); 76 const allRowsObj = JSON.parse(allRowsStr); 77 78 // check count match 79 Assert.equal( 80 allRowsObj.count, 81 seeded.length, 82 "Should return all seeded records" 83 ); 84 85 // check all url match 86 const urls = allRowsObj.results.map(r => r.url).sort(); 87 const expectedUrls = seeded.map(s => s.url).sort(); 88 Assert.deepEqual(urls, expectedUrls, "Should return all seeded URLs"); 89 90 // check title and url match 91 const byUrl = new Map(allRowsObj.results.map(r => [r.url, r])); 92 for (const { url, title } of seeded) { 93 Assert.ok(byUrl.has(url), `Has entry for ${url}`); 94 Assert.equal(byUrl.get(url).title, title, `Title matches for ${url}`); 95 } 96 97 // check visitDate iso string 98 for (const r of allRowsObj.results) { 99 Assert.ok( 100 !isNaN(Date.parse(r.visitDate)), 101 "visitDate is a valid ISO timestamp" 102 ); 103 } 104 }); 105 106 // test: startTs only 107 add_task(async function test_time_range_only_startTs() { 108 await PlacesUtils.history.clear(); 109 110 const now = Date.now(); 111 112 const older = { 113 url: "https://example.com/older", 114 title: "Older Page", 115 visits: [{ date: new Date(now - 60 * 60 * 1000) }], // 60 min ago 116 }; 117 const recent = { 118 url: "https://example.com/recent", 119 title: "Recent Page", 120 visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago 121 }; 122 123 await PlacesUtils.history.insertMany([older, recent]); 124 125 // records after last 10 minutes 126 const startTs = new Date(now - 10 * 60 * 1000).toISOString(); // ISO input 127 128 const rowsStr = await searchBrowsingHistory({ 129 searchTerm: "", 130 startTs, 131 endTs: null, 132 historyLimit: 15, 133 }); 134 const rows = JSON.parse(rowsStr); 135 const urls = rows.results.map(r => r.url); 136 137 Assert.ok( 138 urls.includes(recent.url), 139 "Recent entry should be included when only startTs is set" 140 ); 141 Assert.ok( 142 !urls.includes(older.url), 143 "Older entry should be excluded when only startTs is set" 144 ); 145 }); 146 147 // test: endTs only 148 add_task(async function test_time_range_only_endTs() { 149 await PlacesUtils.history.clear(); 150 151 const now = Date.now(); 152 153 const older = { 154 url: "https://example.com/older", 155 title: "Older Page", 156 visits: [{ date: new Date(now - 60 * 60 * 1000) }], // 60 min ago 157 }; 158 const recent = { 159 url: "https://example.com/recent", 160 title: "Recent Page", 161 visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago 162 }; 163 164 await PlacesUtils.history.insertMany([older, recent]); 165 166 // Anything before last 10 minutes 167 const endTs = new Date(now - 10 * 60 * 1000).toISOString(); // ISO input 168 169 const rowsStr = await searchBrowsingHistory({ 170 searchTerm: "", 171 startTs: null, 172 endTs, 173 historyLimit: 15, 174 }); 175 const rows = JSON.parse(rowsStr); 176 const urls = rows.results.map(r => r.url); 177 178 Assert.ok( 179 urls.includes(older.url), 180 "Older entry should be included when only endTs is set" 181 ); 182 Assert.ok( 183 !urls.includes(recent.url), 184 "Recent entry should be excluded when only endTs is set" 185 ); 186 }); 187 188 // test: startTs + endTs 189 add_task(async function test_time_range_start_and_endTs() { 190 await PlacesUtils.history.clear(); 191 192 const now = Date.now(); 193 194 const beforeWindow = { 195 url: "https://example.com/before-window", 196 title: "Before Window", 197 visits: [{ date: new Date(now - 3 * 60 * 60 * 1000) }], // 3h ago 198 }; 199 const inWindow = { 200 url: "https://example.com/in-window", 201 title: "In Window", 202 visits: [{ date: new Date(now - 30 * 60 * 1000) }], // 30 min ago 203 }; 204 const afterWindow = { 205 url: "https://example.com/after-window", 206 title: "After Window", 207 visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago 208 }; 209 210 await PlacesUtils.history.insertMany([beforeWindow, inWindow, afterWindow]); 211 212 // Time window: [45min ago, 15min ago] 213 const startTs = new Date(now - 45 * 60 * 1000).toISOString(); 214 const endTs = new Date(now - 15 * 60 * 1000).toISOString(); 215 216 const rowsStr = await searchBrowsingHistory({ 217 searchTerm: "", 218 startTs, 219 endTs, 220 historyLimit: 15, 221 }); 222 const rows = JSON.parse(rowsStr); 223 const urls = rows.results.map(r => r.url); 224 225 Assert.ok(urls.includes(inWindow.url), "In window entry should be included"); 226 Assert.ok( 227 !urls.includes(beforeWindow.url), 228 "Before window entry should be excluded" 229 ); 230 Assert.ok( 231 !urls.includes(afterWindow.url), 232 "After window entry should be excluded" 233 ); 234 }); 235 236 /** 237 * Test no results behavior: empty history with and without searchTerm. 238 * 239 * We don't try to force the semantic here (that would require a 240 * running ML engine). Instead we just assert the wrapper's messaging 241 * when there are no rows. 242 */ 243 add_task(async function test_no_results_messages() { 244 await PlacesUtils.history.clear(); 245 246 // No search term: time range message. 247 let outputStr = await searchBrowsingHistory({ 248 searchTerm: "", 249 startTs: null, 250 endTs: null, 251 historyLimit: 15, 252 }); 253 let output = JSON.parse(outputStr); 254 255 Assert.equal(output.results.length, 0, "No results when history is empty"); 256 Assert.ok( 257 output.message.includes("requested time range"), 258 "Message explains empty time-range search" 259 ); 260 261 // With search term: search specific message. 262 outputStr = await searchBrowsingHistory({ 263 searchTerm: "mozilla", 264 startTs: null, 265 endTs: null, 266 historyLimit: 15, 267 }); 268 output = JSON.parse(outputStr); 269 270 Assert.equal(output.results.length, 0, "No results for semantic search"); 271 Assert.ok( 272 output.message.includes("mozilla"), 273 "Message mentions the search term when there are no matches" 274 ); 275 }); 276 277 // test: non-empty searchTerm falls back to basic history search 278 // when semantic search is disabled via prefs. 279 add_task(async function test_basic_text_search_when_semantic_disabled() { 280 await PlacesUtils.history.clear(); 281 282 const now = Date.now(); 283 284 const seeded = [ 285 { 286 url: "https://www.mozilla.org/en-US/", 287 title: "Internet for people, not profit — Mozilla", 288 visits: [{ date: new Date(now - 5 * 60 * 1000) }], // 5 min ago 289 }, 290 { 291 url: "https://example.com/other", 292 title: "Some Other Site", 293 visits: [{ date: new Date(now - 10 * 60 * 1000) }], // 10 min ago 294 }, 295 ]; 296 297 await PlacesUtils.history.insertMany(seeded); 298 299 // Disable semantic search so searchBrowsingHistory must fall back 300 // to the basic history search. 301 Services.prefs.setBoolPref("browser.ml.enable", false); 302 Services.prefs.setBoolPref("places.semanticHistory.featureGate", false); 303 304 const outputStr = await searchBrowsingHistory({ 305 searchTerm: "mozilla", 306 startTs: null, 307 endTs: null, 308 historyLimit: 15, 309 }); 310 const output = JSON.parse(outputStr); 311 312 Assert.equal(output.searchTerm, "mozilla", "searchTerm match"); 313 Assert.equal(output.results.length, 1, "One history entry is returned"); 314 315 const urls = output.results.map(r => r.url); 316 Assert.ok( 317 urls.includes("https://www.mozilla.org/en-US/"), 318 "Basic history search should find the Mozilla entry" 319 ); 320 321 // Restore prefs 322 Services.prefs.setBoolPref("browser.ml.enable", true); 323 Services.prefs.setBoolPref("places.semanticHistory.featureGate", true); 324 });