tor-browser

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

commit 7f79b85c2a365b04978c0754b960a295621c91e3
parent a06e07a18962bfceed8da6aeec09a6e613bef318
Author: Tom Zhang <tzhang@mozilla.com>
Date:   Tue, 30 Dec 2025 01:53:54 +0000

Bug 2006965 - get_open_tabs and get_page_content to only check tabs from ai-window mode r=Mardak,ai-models-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D277618

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 | 411++++---------------------------------------------------------------------------
5 files changed, 137 insertions(+), 576 deletions(-)

diff --git a/browser/components/aiwindow/models/Tools.sys.mjs b/browser/components/aiwindow/models/Tools.sys.mjs @@ -13,6 +13,8 @@ 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", @@ -111,22 +113,25 @@ export const toolsConfig = [ export async function getOpenTabs(n = 15) { const tabs = []; - const win = lazy.BrowserWindowTracker.getTopWindow(); - if (!win || win.closed || !win.gBrowser) { - return []; - } + for (const win of lazy.BrowserWindowTracker.orderedWindows) { + if (!lazy.AIWindow.isAIWindowActive(win)) { + continue; + } - 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, - }); + 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, + }); + } + } } } @@ -270,36 +275,47 @@ export class GetPageContent { ); } - // 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 + // Search through all AI Windows to find the tab with the matching URL let targetTab = null; - for (let i = 0; i < tabs.length; i++) { - const tab = tabs[i]; - const currentURI = tab?.linkedBrowser?.currentURI; - if (currentURI?.spec === url) { - targetTab = tab; - break; + for (const win of lazy.BrowserWindowTracker.orderedWindows) { + if (!lazy.AIWindow.isAIWindowActive(win)) { + continue; } - } - // If no match, try hostname matching for cases where protocols differ - if (!targetTab) { - try { - const inputHostPort = new URL(url).host; - targetTab = tabs.find(tab => { + 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) { try { - const tabHostPort = tab.linkedBrowser.currentURI.hostPort; - return tabHostPort === inputHostPort; + const inputHostPort = new URL(url).host; + targetTab = tabs.find(tab => { + try { + const tabHostPort = tab.linkedBrowser.currentURI.hostPort; + return tabHostPort === inputHostPort; + } catch { + return false; + } + }); } catch { - return false; + // Invalid URL, continue with original logic } - }); - } catch { - // Invalid URL, continue with original logic + } + + // If we found the tab, stop searching + if (targetTab) { + break; + } } } diff --git a/browser/components/aiwindow/models/tests/browser/browser.toml b/browser/components/aiwindow/models/tests/browser/browser.toml @@ -2,5 +2,9 @@ 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,6 +26,20 @@ 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,12 +22,17 @@ function createFakeTab(url, title, lastAccessed) { }; } -function createFakeWindow(tabs, closed = false) { +function createFakeWindow(tabs, closed = false, isAIWindow = true) { return { closed, gBrowser: { tabs, }, + document: { + documentElement: { + hasAttribute: attr => attr === "ai-window" && isAIWindow, + }, + }, }; } @@ -65,7 +70,7 @@ add_task(async function test_getOpenTabs_basic() { createFakeTab("https://firefox.com", "Firefox", 3000), ]); - sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); + sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); setupPageDataServiceMock(sb, { "https://firefox.com": "Firefox browser homepage", "https://mozilla.org": "Mozilla organization site", @@ -114,7 +119,7 @@ add_task(async function test_getOpenTabs_filters_about_urls() { createFakeTab("about:blank", "Blank", 5000), ]); - sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); + sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); setupPageDataServiceMock(sb); const tabs = await getOpenTabs(); @@ -139,7 +144,7 @@ add_task(async function test_getOpenTabs_filters_about_urls() { } }); -add_task(async function test_getOpenTabs_limits_to_n() { +add_task(async function test_getOpenTabs_pagination() { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ).BrowserWindowTracker; @@ -155,19 +160,23 @@ add_task(async function test_getOpenTabs_limits_to_n() { } const fakeWindow = createFakeWindow(tabs); - sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); + sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [fakeWindow]); setupPageDataServiceMock(sb); - const result = await getOpenTabs(10); - - Assert.equal(result.length, 10, "Should return at most 10 tabs"); + // Test default limit + const defaultResult = await getOpenTabs(); + Assert.equal(defaultResult.length, 15, "Should default to 15 tabs"); Assert.equal( - result[0].url, + defaultResult[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( - result[9].url, + customResult[9].url, "https://example10.com", "Last tab should be 10th most recent" ); @@ -176,68 +185,7 @@ add_task(async function test_getOpenTabs_limits_to_n() { } }); -add_task(async function test_getOpenTabs_default_limit() { - const BrowserWindowTracker = ChromeUtils.importESModule( - "resource:///modules/BrowserWindowTracker.sys.mjs" - ).BrowserWindowTracker; - - const sb = sinon.createSandbox(); - - try { - 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() { +add_task(async function test_getOpenTabs_filters_non_ai_windows() { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ).BrowserWindowTracker; @@ -245,87 +193,43 @@ add_task(async function test_getOpenTabs_closed_window() { const sb = sinon.createSandbox(); try { - const closedWindow = createFakeWindow( - [createFakeTab("https://closed.com", "Closed", 2000)], + const aiWindow = createFakeWindow( + [ + createFakeTab("https://ai1.com", "AI Tab 1", 1000), + createFakeTab("https://ai2.com", "AI Tab 2", 2000), + ], + false, true ); - 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 + const classicWindow = createFakeWindow( + [ + createFakeTab("https://classic1.com", "Classic Tab 1", 3000), + createFakeTab("https://classic2.com", "Classic Tab 2", 4000), + ], + false, + false ); + + sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [ + classicWindow, + aiWindow, + ]); setupPageDataServiceMock(sb); const tabs = await getOpenTabs(); Assert.equal( tabs.length, - 0, - "Should return empty array for window without gBrowser" + 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" ); - } 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(); } @@ -343,7 +247,7 @@ add_task(async function test_getOpenTabs_return_structure() { createFakeTab("https://test.com", "Test Page", 1000), ]); - sb.stub(BrowserWindowTracker, "getTopWindow").returns(fakeWindow); + sb.stub(BrowserWindowTracker, "orderedWindows").get(() => [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,21 +42,34 @@ function createFakeTab(url, title, hasBrowsingContext = true) { }; } -function createFakeWindow(tabs) { +function createFakeWindow(tabs, closed = false, isAIWindow = true) { return { - closed: false, + closed, gBrowser: { tabs, }, + document: { + documentElement: { + hasAttribute: attr => attr === "ai-window" && isAIWindow, + }, + }, }; } -function setupBrowserWindowTracker(sandbox, window) { +function setupBrowserWindowTracker(sandbox, windows) { const BrowserWindowTracker = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ).BrowserWindowTracker; - sandbox.stub(BrowserWindowTracker, "getTopWindow").returns(window); + let windowArray; + if (windows === null) { + windowArray = []; + } else if (Array.isArray(windows)) { + windowArray = windows; + } else { + windowArray = [windows]; + } + sandbox.stub(BrowserWindowTracker, "orderedWindows").get(() => windowArray); } add_task(async function test_getPageContent_exact_url_match() { @@ -90,35 +103,6 @@ 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(); @@ -324,53 +308,6 @@ 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(); @@ -443,39 +380,6 @@ 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(); @@ -513,121 +417,6 @@ 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(); @@ -651,169 +440,3 @@ 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(); - } -});