tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 414417ee92315e4fbb07658d53e2e5d6adaa609c
parent 9183dbde2d07549a3cea6b9f2203db1637126dfc
Author: pstanciu <pstanciu@mozilla.com>
Date:   Mon, 29 Dec 2025 23:38:18 +0200

Revert "Bug 2006965 - get_open_tabs and get_page_content to only check tabs from ai-window mode r=Mardak,ai-models-reviewers" for causing lint failures @test_Tools_GetPageContent.js:65:5

This reverts commit fb71d00185798af0def5c049947c2605f283e76d.

Diffstat:
Mbrowser/components/aiwindow/models/Tools.sys.mjs | 94+++++++++++++++++++++++++++++++++----------------------------------------------
Mbrowser/components/aiwindow/models/tests/browser/browser.toml | 4----
Mbrowser/components/aiwindow/models/tests/browser/browser_get_page_content.js | 14--------------
Mbrowser/components/aiwindow/models/tests/xpcshell/test_Tools_GetOpenTabs.js | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mbrowser/components/aiwindow/models/tests/xpcshell/test_Tools_GetPageContent.js | 405++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
5 files changed, 576 insertions(+), 131 deletions(-)

diff --git a/browser/components/aiwindow/models/Tools.sys.mjs b/browser/components/aiwindow/models/Tools.sys.mjs @@ -13,8 +13,6 @@ import { PageExtractorParent } from "resource://gre/actors/PageExtractorParent.s const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - AIWindow: - "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", PageDataService: "moz-src:///browser/components/pagedata/PageDataService.sys.mjs", @@ -113,25 +111,22 @@ export const toolsConfig = [ export async function getOpenTabs(n = 15) { const tabs = []; - for (const win of lazy.BrowserWindowTracker.orderedWindows) { - if (!lazy.AIWindow.isAIWindowActive(win)) { - continue; - } + const win = lazy.BrowserWindowTracker.getTopWindow(); + if (!win || win.closed || !win.gBrowser) { + return []; + } - if (!win.closed && win.gBrowser) { - for (const tab of win.gBrowser.tabs) { - const browser = tab.linkedBrowser; - const url = browser?.currentURI?.spec; - const title = tab.label; - - if (url && !url.startsWith("about:")) { - tabs.push({ - url, - title, - lastAccessed: tab.lastAccessed, - }); - } - } + for (const tab of win.gBrowser.tabs) { + const browser = tab.linkedBrowser; + const url = browser?.currentURI?.spec; + const title = tab.label; + + if (url && !url.startsWith("about:")) { + tabs.push({ + url, + title, + lastAccessed: tab.lastAccessed, + }); } } @@ -275,47 +270,36 @@ export class GetPageContent { ); } - // Search through all AI Windows to find the tab with the matching URL + // TODO: figure out what windows we can access to give permission here, and update this API + let win = lazy.BrowserWindowTracker.getTopWindow(); + let gBrowser = win.gBrowser; + let tabs = gBrowser.tabs; + + // Find the tab with the matching URL in browser let targetTab = null; - for (const win of lazy.BrowserWindowTracker.orderedWindows) { - if (!lazy.AIWindow.isAIWindowActive(win)) { - continue; + for (let i = 0; i < tabs.length; i++) { + const tab = tabs[i]; + const currentURI = tab?.linkedBrowser?.currentURI; + if (currentURI?.spec === url) { + targetTab = tab; + break; } + } - if (!win.closed && win.gBrowser) { - const tabs = win.gBrowser.tabs; - - // Find the tab with the matching URL in this window - for (let i = 0; i < tabs.length; i++) { - const tab = tabs[i]; - const currentURI = tab?.linkedBrowser?.currentURI; - if (currentURI?.spec === url) { - targetTab = tab; - break; - } - } - - // If no match, try hostname matching for cases where protocols differ - if (!targetTab) { + // If no match, try hostname matching for cases where protocols differ + if (!targetTab) { + try { + const inputHostPort = new URL(url).host; + targetTab = tabs.find(tab => { try { - const inputHostPort = new URL(url).host; - targetTab = tabs.find(tab => { - try { - const tabHostPort = tab.linkedBrowser.currentURI.hostPort; - return tabHostPort === inputHostPort; - } catch { - return false; - } - }); + const tabHostPort = tab.linkedBrowser.currentURI.hostPort; + return tabHostPort === inputHostPort; } catch { - // Invalid URL, continue with original logic + return false; } - } - - // If we found the tab, stop searching - if (targetTab) { - break; - } + }); + } catch { + // Invalid URL, continue with original logic } } diff --git a/browser/components/aiwindow/models/tests/browser/browser.toml b/browser/components/aiwindow/models/tests/browser/browser.toml @@ -2,9 +2,5 @@ support-files = [ "head.js", ] -prefs = [ - "browser.aiwindow.enabled=true", -] ["browser_get_page_content.js"] -window_attributes = "ai-window" diff --git a/browser/components/aiwindow/models/tests/browser/browser_get_page_content.js b/browser/components/aiwindow/models/tests/browser/browser_get_page_content.js @@ -26,20 +26,6 @@ add_task(async function test_get_page_content_basic() { const { url, GetPageContent, cleanup } = await setupGetPageContentTest(html); - // Manually set the ai-window attribute for testing - // (in production this is set via window features when opening the window) - window.document.documentElement.setAttribute("ai-window", "true"); - - // Verify we're in an AI Window - const { AIWindow } = ChromeUtils.importESModule( - "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs" - ); - info("Is AI Window: " + AIWindow.isAIWindowActive(window)); - info( - "Window has ai-window attribute: " + - window.document.documentElement.hasAttribute("ai-window") - ); - // Create an allowed URLs set containing the test page const allowedUrls = new Set([url]); diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_Tools_GetOpenTabs.js b/browser/components/aiwindow/models/tests/xpcshell/test_Tools_GetOpenTabs.js @@ -22,17 +22,12 @@ function createFakeTab(url, title, lastAccessed) { }; } -function createFakeWindow(tabs, closed = false, isAIWindow = true) { +function createFakeWindow(tabs, closed = false) { return { closed, gBrowser: { tabs, }, - document: { - documentElement: { - hasAttribute: attr => attr === "ai-window" && isAIWindow, - }, - }, }; } @@ -70,7 +65,7 @@ add_task(async function test_getOpenTabs_basic() { createFakeTab("https://firefox.com", "Firefox", 3000), ]); - sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); + sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); setupPageDataServiceMock(sb, { "https://firefox.com": "Firefox browser homepage", "https://mozilla.org": "Mozilla organization site", @@ -119,7 +114,7 @@ add_task(async function test_getOpenTabs_filters_about_urls() { createFakeTab("about:blank", "Blank", 5000), ]); - sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); + sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); setupPageDataServiceMock(sb); const tabs = await getOpenTabs(); @@ -144,7 +139,7 @@ add_task(async function test_getOpenTabs_filters_about_urls() { } }); -add_task(async function test_getOpenTabs_pagination() { +add_task(async function test_getOpenTabs_limits_to_n() { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ).BrowserWindowTracker; @@ -160,23 +155,19 @@ add_task(async function test_getOpenTabs_pagination() { } const fakeWindow = createFakeWindow(tabs); - sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); + sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); setupPageDataServiceMock(sb); - // Test default limit - const defaultResult = await getOpenTabs(); - Assert.equal(defaultResult.length, 15, "Should default to 15 tabs"); + const result = await getOpenTabs(10); + + Assert.equal(result.length, 10, "Should return at most 10 tabs"); Assert.equal( - defaultResult[0].url, + result[0].url, "https://example19.com", "First tab should be most recent" ); - - // Test custom limit - const customResult = await getOpenTabs(10); - Assert.equal(customResult.length, 10, "Should return at most 10 tabs"); Assert.equal( - customResult[9].url, + result[9].url, "https://example10.com", "Last tab should be 10th most recent" ); @@ -185,7 +176,7 @@ add_task(async function test_getOpenTabs_pagination() { } }); -add_task(async function test_getOpenTabs_filters_non_ai_windows() { +add_task(async function test_getOpenTabs_default_limit() { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ).BrowserWindowTracker; @@ -193,48 +184,153 @@ add_task(async function test_getOpenTabs_filters_non_ai_windows() { const sb = sinon.createSandbox(); try { - const aiWindow = createFakeWindow( - [ - createFakeTab("https://ai1.com", "AI Tab 1", 1000), - createFakeTab("https://ai2.com", "AI Tab 2", 2000), - ], - false, - true + const tabs = []; + for (let i = 0; i < 20; i++) { + tabs.push( + createFakeTab(`https://example${i}.com`, `Example ${i}`, i * 1000) + ); + } + const fakeWindow = createFakeWindow(tabs); + + sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); + setupPageDataServiceMock(sb); + + const result = await getOpenTabs(); + + Assert.equal(result.length, 15, "Should default to 15 tabs"); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getOpenTabs_single_window_multiple_tabs() { + const BrowserWindowTracker = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ).BrowserWindowTracker; + + const sb = sinon.createSandbox(); + + try { + const window1 = createFakeWindow([ + createFakeTab("https://tab1.com", "Tab1", 1000), + createFakeTab("https://tab2.com", "Tab2", 2000), + createFakeTab("https://tab3.com", "Tab3", 3000), + createFakeTab("https://tab4.com", "Tab4", 4000), + ]); + + sb.stub(BrowserWindowTracker, "getTopWindow").returns(window1); + setupPageDataServiceMock(sb); + + const tabs = await getOpenTabs(); + + Assert.equal(tabs.length, 4, "Should return all tabs from current window"); + Assert.equal( + tabs[0].url, + "https://tab4.com", + "Most recent tab from current window" ); + Assert.equal(tabs[1].url, "https://tab3.com", "Second most recent"); + Assert.equal(tabs[2].url, "https://tab2.com", "Third most recent"); + Assert.equal(tabs[3].url, "https://tab1.com", "Least recent"); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getOpenTabs_closed_window() { + const BrowserWindowTracker = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ).BrowserWindowTracker; + + const sb = sinon.createSandbox(); - const classicWindow = createFakeWindow( - [ - createFakeTab("https://classic1.com", "Classic Tab 1", 3000), - createFakeTab("https://classic2.com", "Classic Tab 2", 4000), - ], - false, - false + try { + const closedWindow = createFakeWindow( + [createFakeTab("https://closed.com", "Closed", 2000)], + true ); - sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [ - classicWindow, - aiWindow, - ]); + sb.stub(BrowserWindowTracker, "getTopWindow").returns(closedWindow); + setupPageDataServiceMock(sb); + + const tabs = await getOpenTabs(); + + Assert.equal(tabs.length, 0, "Should return empty array for closed window"); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getOpenTabs_window_without_gBrowser() { + const BrowserWindowTracker = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ).BrowserWindowTracker; + + const sb = sinon.createSandbox(); + + try { + const windowWithoutGBrowser = { + closed: false, + gBrowser: null, + }; + + sb.stub(BrowserWindowTracker, "getTopWindow").returns( + windowWithoutGBrowser + ); setupPageDataServiceMock(sb); const tabs = await getOpenTabs(); Assert.equal( tabs.length, - 2, - "Should only return tabs from AI Windows (filtered 2 classic tabs)" - ); - Assert.equal(tabs[0].url, "https://ai2.com", "Most recent AI tab"); - Assert.equal(tabs[1].url, "https://ai1.com", "Second AI tab"); - Assert.ok( - !tabs.some(t => t.url.includes("classic")), - "No classic window tabs in results" + 0, + "Should return empty array for window without gBrowser" ); } finally { sb.restore(); } }); +add_task(async function test_getOpenTabs_empty_window() { + const BrowserWindowTracker = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ).BrowserWindowTracker; + + const sb = sinon.createSandbox(); + + try { + const emptyWindow = createFakeWindow([]); + + sb.stub(BrowserWindowTracker, "getTopWindow").returns(emptyWindow); + setupPageDataServiceMock(sb); + + const tabs = await getOpenTabs(); + + Assert.equal(tabs.length, 0, "Should return empty array for no tabs"); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getOpenTabs_no_window() { + const BrowserWindowTracker = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ).BrowserWindowTracker; + + const sb = sinon.createSandbox(); + + try { + sb.stub(BrowserWindowTracker, "getTopWindow").returns(null); + setupPageDataServiceMock(sb); + + const tabs = await getOpenTabs(); + + Assert.equal(tabs.length, 0, "Should return empty array when no window"); + } finally { + sb.restore(); + } +}); + add_task(async function test_getOpenTabs_return_structure() { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" @@ -247,7 +343,7 @@ add_task(async function test_getOpenTabs_return_structure() { createFakeTab("https://test.com", "Test Page", 1000), ]); - sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); + sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); setupPageDataServiceMock(sb, { "https://test.com": "A test page description", }); diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_Tools_GetPageContent.js b/browser/components/aiwindow/models/tests/xpcshell/test_Tools_GetPageContent.js @@ -42,28 +42,21 @@ function createFakeTab(url, title, hasBrowsingContext = true) { }; } -function createFakeWindow(tabs, closed = false, isAIWindow = true) { +function createFakeWindow(tabs) { return { - closed, + closed: false, gBrowser: { tabs, }, - document: { - documentElement: { - hasAttribute: attr => attr === "ai-window" && isAIWindow, - }, - }, }; } -function setupBrowserWindowTracker(sandbox, windows) { +function setupBrowserWindowTracker(sandbox, window) { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ).BrowserWindowTracker; - const windowArray = - windows === null ? [] : Array.isArray(windows) ? windows : [windows]; - sandbox.stub(BrowserWindowTracker, "orderedWindows").get(() => windowArray); + sandbox.stub(BrowserWindowTracker, "getTopWindow").returns(window); } add_task(async function test_getPageContent_exact_url_match() { @@ -97,6 +90,35 @@ add_task(async function test_getPageContent_exact_url_match() { } }); +add_task(async function test_getPageContent_normalized_url_match() { + const sb = sinon.createSandbox(); + + try { + const tabs = [ + createFakeTab("https://example.com/page/", "Example Page"), + createFakeTab("https://other.com", "Other"), + ]; + + setupBrowserWindowTracker(sb, createFakeWindow(tabs)); + + const result = await GetPageContent.getPageContent( + { url: "https://example.com/page" }, + new Set(["https://example.com/page"]) + ); + + Assert.ok( + result.includes("Example Page"), + "Should match URL after normalizing trailing slashes" + ); + Assert.ok( + result.includes("Sample page content"), + "Should include page content" + ); + } finally { + sb.restore(); + } +}); + add_task(async function test_getPageContent_hostname_match() { const sb = sinon.createSandbox(); @@ -302,6 +324,53 @@ add_task(async function test_getPageContent_content_truncation() { } }); +add_task(async function test_getPageContent_truncation_at_sentence_boundary() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com/sentences"; + const sentence = "This is a sentence. "; + const longContent = sentence.repeat(600); + + const mockExtractor = { + getText: sinon.stub().resolves(longContent), + getReaderModeContent: sinon.stub().resolves(""), + }; + + const tab = createFakeTab(targetUrl, "Sentences"); + tab.linkedBrowser.browsingContext.currentWindowContext.getActor = sinon + .stub() + .resolves(mockExtractor); + + setupBrowserWindowTracker(sb, createFakeWindow([tab])); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + const contentMatch = result.match(/Content \(full page\) from.*:\s*(.*)/s); + Assert.ok(contentMatch, "Should match content pattern"); + + const extractedContent = contentMatch[1].trim(); + Assert.lessOrEqual( + extractedContent.length, + 10001, + "Should truncate near 10000 chars" + ); + Assert.ok( + extractedContent.endsWith("."), + "Should end at sentence boundary (period)" + ); + Assert.ok( + !extractedContent.endsWith("..."), + "Should not have ... when truncated at sentence" + ); + } finally { + sb.restore(); + } +}); + add_task(async function test_getPageContent_empty_content() { const sb = sinon.createSandbox(); @@ -374,6 +443,39 @@ add_task(async function test_getPageContent_extraction_error() { } }); +add_task(async function test_getPageContent_viewport_mode() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com/viewport"; + + const mockExtractor = { + getText: sinon.stub().resolves("Full page content"), + getReaderModeContent: sinon.stub().resolves(""), + }; + + const tab = createFakeTab(targetUrl, "Viewport Test"); + tab.linkedBrowser.browsingContext.currentWindowContext.getActor = sinon + .stub() + .resolves(mockExtractor); + + setupBrowserWindowTracker(sb, createFakeWindow([tab])); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes("Content (full page)"), + "Should use full mode by default" + ); + Assert.ok(result.includes("Full page content"), "Should include content"); + } finally { + sb.restore(); + } +}); + add_task(async function test_getPageContent_reader_mode_string() { const sb = sinon.createSandbox(); @@ -411,6 +513,121 @@ add_task(async function test_getPageContent_reader_mode_string() { } }); +add_task(async function test_getPageContent_no_window() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com"; + setupBrowserWindowTracker(sb, null); + + // Add URL to allowed list so it checks for window instead of trying headless + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes("Error retrieving content"), + "Should handle null window gracefully" + ); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getPageContent_closed_window() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com"; + const closedWindow = { + closed: true, + gBrowser: { tabs: [] }, + }; + + setupBrowserWindowTracker(sb, closedWindow); + + // Add URL to allowed list so it checks for window instead of trying headless + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes("Error retrieving content") || + result.includes("Cannot find URL"), + "Should handle closed window with error" + ); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getPageContent_window_without_gBrowser() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com"; + const windowWithoutGBrowser = { + closed: false, + gBrowser: null, + }; + + setupBrowserWindowTracker(sb, windowWithoutGBrowser); + + // Add URL to allowed list so it checks for window instead of trying headless + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes("Error retrieving content"), + "Should handle window without gBrowser" + ); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getPageContent_whitespace_normalization() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com/whitespace"; + const messyContent = + "Text with lots\n\n\nof whitespace\n\n\n\nhere"; + + const mockExtractor = { + getText: sinon.stub().resolves(messyContent), + getReaderModeContent: sinon.stub().resolves(""), + }; + + const tab = createFakeTab(targetUrl, "Whitespace Test"); + tab.linkedBrowser.browsingContext.currentWindowContext.getActor = sinon + .stub() + .resolves(mockExtractor); + + setupBrowserWindowTracker(sb, createFakeWindow([tab])); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes("Text with lots of whitespace here"), + "Should normalize whitespace" + ); + Assert.ok( + !result.includes(" "), + "Should not have multiple consecutive spaces" + ); + } finally { + sb.restore(); + } +}); + add_task(async function test_getPageContent_invalid_url_format() { const sb = sinon.createSandbox(); @@ -434,3 +651,169 @@ add_task(async function test_getPageContent_invalid_url_format() { sb.restore(); } }); + +add_task(async function test_getPageContent_extraction_returns_string() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com/string"; + const directString = "Direct string content"; + + const mockExtractor = { + getText: sinon.stub().resolves(directString), + getReaderModeContent: sinon.stub().resolves(""), + }; + + const tab = createFakeTab(targetUrl, "String Test"); + tab.linkedBrowser.browsingContext.currentWindowContext.getActor = sinon + .stub() + .resolves(mockExtractor); + + setupBrowserWindowTracker(sb, createFakeWindow([tab])); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes(directString), + "Should handle extraction returning string directly" + ); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getPageContent_extraction_returns_object() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com/object"; + // The API now expects strings, not objects + // If getText returns a non-string object, it should be treated as no content + const objectContent = { text: "Object text content" }; + + const mockExtractor = { + getText: sinon.stub().resolves(objectContent), + getReaderModeContent: sinon.stub().resolves(""), + }; + + const tab = createFakeTab(targetUrl, "Object Test"); + tab.linkedBrowser.browsingContext.currentWindowContext.getActor = sinon + .stub() + .resolves(mockExtractor); + + setupBrowserWindowTracker(sb, createFakeWindow([tab])); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + // API expects strings now, objects are treated as no content + Assert.ok( + result.includes("returned no content"), + "Should treat object return value as no content" + ); + } finally { + sb.restore(); + } +}); + +add_task( + async function test_getPageContent_extraction_returns_non_string_text() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://example.com/nonstring"; + + const mockExtractor = { + getText: sinon.stub().resolves(12345), + getReaderModeContent: sinon.stub().resolves(""), + }; + + const tab = createFakeTab(targetUrl, "Non-string Test"); + tab.linkedBrowser.browsingContext.currentWindowContext.getActor = sinon + .stub() + .resolves(mockExtractor); + + setupBrowserWindowTracker(sb, createFakeWindow([tab])); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + Assert.ok( + result.includes("returned no content"), + "Should handle non-string text property as empty" + ); + } finally { + sb.restore(); + } + } +); + +add_task(async function test_getPageContent_allowed_urls_set() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://allowed.com/page"; + const tabs = [createFakeTab("https://other.com", "Other")]; + + setupBrowserWindowTracker(sb, createFakeWindow(tabs)); + + const allowedUrls = new Set([ + "https://allowed.com/page", + "https://another-allowed.com", + ]); + + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + allowedUrls + ); + + // Headless extraction doesn't work in xpcshell environment + Assert.ok( + result.includes("Cannot find URL"), + "Should return error when tab not found (headless doesn't work in xpcshell)" + ); + } finally { + sb.restore(); + } +}); + +add_task(async function test_getPageContent_available_tabs_list() { + const sb = sinon.createSandbox(); + + try { + const targetUrl = "https://notfound.com"; + const tabs = [ + createFakeTab("https://first.com", "First Tab"), + createFakeTab("https://second.com", "Second Tab"), + createFakeTab("https://third.com", "Third Tab"), + createFakeTab("https://fourth.com", "Fourth Tab"), + ]; + + setupBrowserWindowTracker(sb, createFakeWindow(tabs)); + + // Add the URL to allowed list so it searches tabs instead of trying headless + const result = await GetPageContent.getPageContent( + { url: targetUrl }, + new Set([targetUrl]) + ); + + // URL is in allowed list but not open, so should get error + Assert.ok( + result.includes("Cannot find URL"), + "Should return error when tab not found" + ); + Assert.ok( + result.includes(targetUrl), + "Should include requested URL in error" + ); + } finally { + sb.restore(); + } +});