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:
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();
+ }
+});