Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* 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/. */
// Import TabManagementService class
const { TabManagementService } = ChromeUtils.importESModule(
"moz-src:///browser/components/aiwindow/ui/modules/TabManagementService.sys.mjs"
);
// Mock SessionStore for testing
const mockSessionStore = {
closedTabs: [],
undoCloseTab(_window, index) {
if (index >= 0 && index < this.closedTabs.length) {
const restoredTab = this.closedTabs.splice(index, 1)[0];
return {
linkedBrowser: {
currentURI: { spec: restoredTab.state.entries[0].url },
},
label: restoredTab.state.entries[0].title,
};
}
return null;
},
getClosedTabDataForWindow() {
return [...this.closedTabs];
},
addClosedTab(tab) {
this.closedTabs.unshift(tab);
},
reset() {
this.closedTabs = [];
},
};
// Create TabManagementService instance with mock SessionStore
const tabManagementService = new TabManagementService(mockSessionStore);
/**
* Helper to create a mock tab
*
* @param {string|null} url - Tab URL
* @param {string} title - Tab title
* @param {number} userContextId - Container ID (0 = default)
* @param {boolean} isClosing - Whether tab is closing
* @returns {object} Mock tab object
*/
function createMockTab(url, title, userContextId = 0, isClosing = false) {
const mockTab = {
linkedBrowser: {
currentURI: { spec: url },
contentPrincipal: {
originAttributes: { userContextId },
},
},
label: title,
userContextId,
closing: isClosing,
documentGlobal: null, // Will be set per test
getAttribute: () => null,
_tPos: 0,
};
return mockTab;
}
/**
* Helper to create a mock window
*/
function createMockWindow() {
const removedTabs = [];
const mockWindow = {
gBrowser: {
tabs: [],
removeTab(tab) {
const index = this.tabs.indexOf(tab);
if (index > -1) {
const removed = this.tabs.splice(index, 1)[0];
removedTabs.push(removed);
// Simulate SessionStore adding the closed tab
mockSessionStore.addClosedTab({
state: {
entries: [
{
url: removed.linkedBrowser.currentURI.spec,
title: removed.label,
},
],
index: 1,
userContextId: removed.userContextId || 0,
},
});
}
},
},
location: { href: "chrome://browser/content/browser.xhtml" },
_removedTabs: removedTabs,
};
return mockWindow;
}
/**
* Test that tabManagementService is a singleton
*/
add_task(async function test_tabmanagement_service_singleton() {
const service1 = tabManagementService;
const service2 = tabManagementService;
Assert.equal(
service1,
service2,
"tabManagementService should be a singleton"
);
});
/**
* Test closing tabs and getting an operation ID
*/
add_task(async function test_close_tabs_returns_operation_id() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
const tab1 = createMockTab("https://example.com", "Example");
const tab2 = createMockTab("https://mozilla.org", "Mozilla");
// Set the window reference for validation
tab1.documentGlobal = mockWindow;
tab2.documentGlobal = mockWindow;
mockWindow.gBrowser.tabs = [tab1, tab2];
const result = await tabManagementService.closeTabs({
tabs: [tab1, tab2],
window: mockWindow,
});
Assert.equal(result.requestedCount, 2, "Requested to close 2 tabs");
Assert.ok(result.operationId, "Should return an operation ID");
Assert.ok(
result.operationId.startsWith("tab-close-"),
"Operation ID should have correct prefix"
);
Assert.equal(result.failedTabs.length, 0, "Should have no failed tabs");
// Verify tabs were added to SessionStore
Assert.equal(
mockSessionStore.closedTabs.length,
2,
"SessionStore should have 2 closed tabs"
);
});
/**
* Test restoring tabs using operation ID
*/
add_task(async function test_restore_tabs_with_operation_id() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
const tab1 = createMockTab("https://example.com", "Example");
const tab2 = createMockTab("https://mozilla.org", "Mozilla");
tab1.documentGlobal = mockWindow;
tab2.documentGlobal = mockWindow;
mockWindow.gBrowser.tabs = [tab1, tab2];
// Close tabs
const closeResult = await tabManagementService.closeTabs({
tabs: [tab1, tab2],
window: mockWindow,
});
Assert.ok(closeResult.operationId, "Close should return operation ID");
// Restore tabs
const restoreResult = await tabManagementService.restoreTabs({
operationId: closeResult.operationId,
window: mockWindow,
});
Assert.equal(restoreResult.restoredCount, 2, "Should restore 2 tabs");
Assert.equal(restoreResult.requestedCount, 2, "Requested to restore 2 tabs");
Assert.equal(
restoreResult.failedTabs.length,
0,
"Should have no failed tabs"
);
Assert.equal(
restoreResult.restoredTabs.length,
2,
"Should have 2 restored tab objects"
);
});
/**
* Test that invalid tabs are filtered out
*/
add_task(async function test_invalid_tabs_filtered() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
const validTab = createMockTab("https://example.com", "Valid");
const invalidTab = createMockTab("https://invalid.com", "Invalid");
const closingTab = createMockTab("https://closing.com", "Closing", 0, true);
validTab.documentGlobal = mockWindow;
invalidTab.documentGlobal = null; // Wrong window
closingTab.documentGlobal = mockWindow;
const result = await tabManagementService.closeTabs({
tabs: [validTab, invalidTab, closingTab],
window: mockWindow,
});
Assert.equal(result.requestedCount, 3, "Requested to close 3 tabs");
Assert.equal(result.failedTabs.length, 2, "Should have 2 failed tabs");
// Check failure reasons
const failureReasons = result.failedTabs.map(f => f.reason);
Assert.ok(
failureReasons.includes("invalid-tab"),
"Should have invalid-tab failure"
);
Assert.ok(
failureReasons.includes("already-closing"),
"Should have already-closing failure"
);
});
/**
* Test restoring with invalid operation ID
*/
add_task(async function test_restore_invalid_operation_id() {
const mockWindow = createMockWindow();
const result = await tabManagementService.restoreTabs({
operationId: "invalid-id",
window: mockWindow,
});
Assert.equal(result.restoredCount, 0, "Should restore 0 tabs");
Assert.equal(result.requestedCount, 0, "Should request 0 tabs");
Assert.equal(result.failedTabs.length, 0, "Should have no failed tabs");
});
/**
* Test that container tabs are matched correctly
*/
add_task(async function test_container_tab_matching() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
const containerTab = createMockTab("https://example.com", "Container Tab", 2);
containerTab.documentGlobal = mockWindow;
mockWindow.gBrowser.tabs = [containerTab];
// Close the container tab
const closeResult = await tabManagementService.closeTabs({
tabs: [containerTab],
window: mockWindow,
});
Assert.ok(closeResult.operationId, "Should return operation ID");
Assert.equal(closeResult.requestedCount, 1, "Requested to close 1 tab");
// Verify the closed tab in SessionStore has correct userContextId
Assert.equal(
mockSessionStore.closedTabs[0].state.userContextId,
2,
"Closed tab should preserve container ID"
);
// Restore the container tab
const restoreResult = await tabManagementService.restoreTabs({
operationId: closeResult.operationId,
window: mockWindow,
});
Assert.equal(restoreResult.restoredCount, 1, "Should restore container tab");
});
/**
* Test operation limit (max 10 stored operations)
*/
add_task(async function test_operation_limit() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
const operationIds = [];
// Create more than 10 operations
for (let i = 0; i < 12; i++) {
const tab = createMockTab(`https://example${i}.com`, `Tab ${i}`);
tab.documentGlobal = mockWindow;
mockWindow.gBrowser.tabs = [tab];
const result = await tabManagementService.closeTabs({
tabs: [tab],
window: mockWindow,
});
if (result.operationId) {
operationIds.push(result.operationId);
}
}
// First two operations should be evicted
const firstOpResult = await tabManagementService.restoreTabs({
operationId: operationIds[0],
window: mockWindow,
});
Assert.equal(
firstOpResult.restoredCount,
0,
"First operation should be evicted (exceeded limit)"
);
// Recent operations should still work
const recentOpResult = await tabManagementService.restoreTabs({
operationId: operationIds[11],
window: mockWindow,
});
// Note: This might fail if SessionStore doesn't have the tab anymore
// In real usage, SessionStore would have the closed tab
Assert.greater(
recentOpResult.requestedCount,
0,
"Recent operation should still be stored"
);
});
/**
* Test handling of tabs with no URL
*/
add_task(async function test_tabs_without_url() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
const tabNoUrl = createMockTab(null, "No URL");
tabNoUrl.documentGlobal = mockWindow;
const tabWithUrl = createMockTab("https://example.com", "With URL");
tabWithUrl.documentGlobal = mockWindow;
mockWindow.gBrowser.tabs = [tabNoUrl, tabWithUrl];
const result = await tabManagementService.closeTabs({
tabs: [tabNoUrl, tabWithUrl],
window: mockWindow,
});
// Both tabs should close (null URL is valid for about: pages, etc)
Assert.equal(result.requestedCount, 2, "Requested to close 2 tabs");
Assert.equal(result.failedTabs.length, 0, "Should have no failed tabs");
});
/**
* Test error handling when window is invalid
*/
add_task(async function test_invalid_window() {
await Assert.rejects(
tabManagementService.closeTabs({
tabs: [createMockTab("https://example.com", "Test")],
window: null,
}),
/Invalid browser window/,
"Should throw for null window"
);
await Assert.rejects(
tabManagementService.closeTabs({
tabs: [createMockTab("https://example.com", "Test")],
window: {}, // No gBrowser
}),
/Invalid browser window/,
"Should throw for window without gBrowser"
);
});
/**
* Test empty tabs array
*/
add_task(async function test_empty_tabs_array() {
const mockWindow = createMockWindow();
const result = await tabManagementService.closeTabs({
tabs: [],
window: mockWindow,
});
Assert.equal(result.requestedCount, 0, "Should request 0 tabs");
Assert.equal(result.operationId, null, "Should not return operation ID");
Assert.equal(result.failedTabs.length, 0, "Should have no failed tabs");
});
/**
* Test timestamp disambiguation when multiple tabs with same URL exist
*/
add_task(async function test_timestamp_disambiguation() {
mockSessionStore.reset();
const mockWindow = createMockWindow();
// Simulate an older tab closed by AI (30 minutes ago)
const aiOperationTime = Date.now() - 30 * 60 * 1000;
mockSessionStore.addClosedTab({
closedAt: aiOperationTime,
state: {
entries: [
{
title: "NYC Hotels - Old",
},
],
index: 1,
userContextId: 0,
},
});
// Simulate a newer tab closed manually by user (5 minutes ago)
const userClosedTime = Date.now() - 5 * 60 * 1000;
mockSessionStore.addClosedTab({
closedAt: userClosedTime,
state: {
entries: [
{
title: "NYC Hotels - New",
},
],
index: 1,
userContextId: 0,
},
});
// Store a fake operation with the AI operation timestamp
const operationId = tabManagementService.storeClosedTabsForUndo({
closedTabs: [
{
title: "NYC Hotels - Old",
userContextId: 0,
operationTimestamp: aiOperationTime,
},
],
});
// Restore should pick the older tab (closest to operationTimestamp)
const restoreResult = await tabManagementService.restoreTabs({
operationId,
window: mockWindow,
});
Assert.equal(restoreResult.restoredCount, 1, "Should restore 1 tab");
Assert.equal(
restoreResult.restoredTabs[0].label,
"NYC Hotels - Old",
"Should restore the older tab that matches operation timestamp"
);
// Verify the correct tab was removed from mockSessionStore
Assert.equal(
mockSessionStore.closedTabs.length,
1,
"Should have 1 tab remaining in closed tabs"
);
Assert.equal(
mockSessionStore.closedTabs[0].state.entries[0].title,
"NYC Hotels - New",
"The newer manually-closed tab should remain"
);
});