diff options
Diffstat (limited to 'browser/components/extensions/test')
119 files changed, 14599 insertions, 0 deletions
diff --git a/browser/components/extensions/test/browser/.eslintrc.js b/browser/components/extensions/test/browser/.eslintrc.js new file mode 100644 index 000000000..0e673ecb9 --- /dev/null +++ b/browser/components/extensions/test/browser/.eslintrc.js @@ -0,0 +1,36 @@ +"use strict"; + +module.exports = { // eslint-disable-line no-undef + "extends": "../../../../../testing/mochitest/browser.eslintrc.js", + + "env": { + "webextensions": true, + }, + + "globals": { + "NetUtil": true, + "XPCOMUtils": true, + "Task": true, + + // Browser window globals. + "PanelUI": false, + + // Test harness globals + "ExtensionTestUtils": false, + "TestUtils": false, + + "clickBrowserAction": true, + "clickPageAction": true, + "closeContextMenu": true, + "closeExtensionContextMenu": true, + "focusWindow": true, + "makeWidgetId": true, + "openContextMenu": true, + "openExtensionContextMenu": true, + "CustomizableUI": true, + }, + + "rules": { + "no-shadow": 0, + }, +}; diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini new file mode 100644 index 000000000..1e894dcb5 --- /dev/null +++ b/browser/components/extensions/test/browser/browser.ini @@ -0,0 +1,115 @@ +[DEFAULT] +support-files = + head.js + head_pageAction.js + head_sessions.js + context.html + ctxmenu-image.png + context_tabs_onUpdated_page.html + context_tabs_onUpdated_iframe.html + file_popup_api_injection_a.html + file_popup_api_injection_b.html + file_iframe_document.html + file_iframe_document.sjs + file_bypass_cache.sjs + file_language_fr_en.html + file_language_ja.html + file_language_tlh.html + file_dummy.html + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js +tags = webextensions + + +[browser_ext_browserAction_context.js] +[browser_ext_browserAction_disabled.js] +[browser_ext_browserAction_pageAction_icon.js] +[browser_ext_browserAction_pageAction_icon_permissions.js] +[browser_ext_browserAction_popup.js] +[browser_ext_browserAction_popup_resize.js] +[browser_ext_browserAction_simple.js] +[browser_ext_commands_execute_browser_action.js] +[browser_ext_commands_execute_page_action.js] +[browser_ext_commands_getAll.js] +[browser_ext_commands_onCommand.js] +[browser_ext_contentscript_connect.js] +[browser_ext_contextMenus.js] +[browser_ext_contextMenus_checkboxes.js] +[browser_ext_contextMenus_icons.js] +[browser_ext_contextMenus_onclick.js] +[browser_ext_contextMenus_radioGroups.js] +[browser_ext_contextMenus_uninstall.js] +[browser_ext_contextMenus_urlPatterns.js] +[browser_ext_currentWindow.js] +[browser_ext_getViews.js] +[browser_ext_incognito_popup.js] +[browser_ext_incognito_views.js] +[browser_ext_lastError.js] +[browser_ext_legacy_extension_context_contentscript.js] +[browser_ext_omnibox.js] +[browser_ext_optionsPage_privileges.js] +[browser_ext_pageAction_context.js] +[browser_ext_pageAction_popup.js] +[browser_ext_pageAction_popup_resize.js] +[browser_ext_pageAction_simple.js] +[browser_ext_pageAction_title.js] +[browser_ext_popup_api_injection.js] +[browser_ext_popup_background.js] +[browser_ext_popup_corners.js] +[browser_ext_popup_sendMessage.js] +[browser_ext_popup_shutdown.js] +[browser_ext_runtime_openOptionsPage.js] +[browser_ext_runtime_openOptionsPage_uninstall.js] +[browser_ext_runtime_setUninstallURL.js] +[browser_ext_sessions_getRecentlyClosed.js] +[browser_ext_sessions_getRecentlyClosed_private.js] +[browser_ext_sessions_getRecentlyClosed_tabs.js] +[browser_ext_sessions_restore.js] +[browser_ext_simple.js] +[browser_ext_tab_runtimeConnect.js] +[browser_ext_tabs_audio.js] +[browser_ext_tabs_captureVisibleTab.js] +[browser_ext_tabs_create.js] +[browser_ext_tabs_create_invalid_url.js] +[browser_ext_tabs_detectLanguage.js] +[browser_ext_tabs_duplicate.js] +[browser_ext_tabs_events.js] +[browser_ext_tabs_executeScript.js] +[browser_ext_tabs_executeScript_good.js] +[browser_ext_tabs_executeScript_bad.js] +[browser_ext_tabs_executeScript_runAt.js] +[browser_ext_tabs_getCurrent.js] +[browser_ext_tabs_insertCSS.js] +[browser_ext_tabs_removeCSS.js] +[browser_ext_tabs_move.js] +[browser_ext_tabs_move_window.js] +[browser_ext_tabs_move_window_multiple.js] +[browser_ext_tabs_move_window_pinned.js] +[browser_ext_tabs_onHighlighted.js] +[browser_ext_tabs_onUpdated.js] +[browser_ext_tabs_query.js] +[browser_ext_tabs_reload.js] +[browser_ext_tabs_reload_bypass_cache.js] +[browser_ext_tabs_sendMessage.js] +[browser_ext_tabs_cookieStoreId.js] +[browser_ext_tabs_update.js] +[browser_ext_tabs_zoom.js] +[browser_ext_tabs_update_url.js] +[browser_ext_topwindowid.js] +[browser_ext_webNavigation_frameId0.js] +[browser_ext_webNavigation_getFrames.js] +[browser_ext_webNavigation_urlbar_transitions.js] +[browser_ext_webRequest.js] +[browser_ext_windows.js] +[browser_ext_windows_allowScriptsToClose.js] +[browser_ext_windows_create.js] +tags = fullscreen +[browser_ext_windows_create_params.js] +[browser_ext_windows_create_tabId.js] +[browser_ext_windows_create_url.js] +[browser_ext_windows_events.js] +[browser_ext_windows_size.js] +skip-if = os == 'mac' # Fails when windows are randomly opened in fullscreen mode +[browser_ext_windows_update.js] +tags = fullscreen diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js new file mode 100644 index 000000000..8a26dbb3c --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js @@ -0,0 +1,398 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* runTests(options) { + async function background(getTests) { + async function checkDetails(expecting, tabId) { + let title = await browser.browserAction.getTitle({tabId}); + browser.test.assertEq(expecting.title, title, + "expected value from getTitle"); + + let popup = await browser.browserAction.getPopup({tabId}); + browser.test.assertEq(expecting.popup, popup, + "expected value from getPopup"); + + let badge = await browser.browserAction.getBadgeText({tabId}); + browser.test.assertEq(expecting.badge, badge, + "expected value from getBadge"); + + let badgeBackgroundColor = await browser.browserAction.getBadgeBackgroundColor({tabId}); + browser.test.assertEq(String(expecting.badgeBackgroundColor), + String(badgeBackgroundColor), + "expected value from getBadgeBackgroundColor"); + } + + let expectDefaults = expecting => { + return checkDetails(expecting); + }; + + let tabs = []; + let tests = getTests(tabs, expectDefaults); + + { + let tabId = 0xdeadbeef; + let calls = [ + () => browser.browserAction.enable(tabId), + () => browser.browserAction.disable(tabId), + () => browser.browserAction.setTitle({tabId, title: "foo"}), + () => browser.browserAction.setIcon({tabId, path: "foo.png"}), + () => browser.browserAction.setPopup({tabId, popup: "foo.html"}), + () => browser.browserAction.setBadgeText({tabId, text: "foo"}), + () => browser.browserAction.setBadgeBackgroundColor({tabId, color: [0xff, 0, 0, 0xff]}), + ]; + + for (let call of calls) { + await browser.test.assertRejects( + new Promise(resolve => resolve(call())), + RegExp(`Invalid tab ID: ${tabId}`), + "Expected invalid tab ID error"); + } + } + + // Runs the next test in the `tests` array, checks the results, + // and passes control back to the outer test scope. + function nextTest() { + let test = tests.shift(); + + test(async expecting => { + // Check that the API returns the expected values, and then + // run the next test. + let tabs = await browser.tabs.query({active: true, currentWindow: true}); + await checkDetails(expecting, tabs[0].id); + + // Check that the actual icon has the expected values, then + // run the next test. + browser.test.sendMessage("nextTest", expecting, tests.length); + }); + } + + browser.test.onMessage.addListener((msg) => { + if (msg != "runNextTest") { + browser.test.fail("Expecting 'runNextTest' message"); + } + + nextTest(); + }); + + browser.tabs.query({active: true, currentWindow: true}, resultTabs => { + tabs[0] = resultTabs[0].id; + + nextTest(); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: options.manifest, + + files: options.files || {}, + + background: `(${background})(${options.getTests})`, + }); + + let browserActionId; + function checkDetails(details) { + if (!browserActionId) { + browserActionId = `${makeWidgetId(extension.id)}-browser-action`; + } + + let button = document.getElementById(browserActionId); + + ok(button, "button exists"); + + let title = details.title || options.manifest.name; + + is(getListStyleImage(button), details.icon, "icon URL is correct"); + is(button.getAttribute("tooltiptext"), title, "image title is correct"); + is(button.getAttribute("label"), title, "image label is correct"); + is(button.getAttribute("badge"), details.badge, "badge text is correct"); + is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct"); + + if (details.badge && details.badgeBackgroundColor) { + let badge = button.ownerDocument.getAnonymousElementByAttribute( + button, "class", "toolbarbutton-badge"); + + let badgeColor = window.getComputedStyle(badge).backgroundColor; + let color = details.badgeBackgroundColor; + let expectedColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; + + is(badgeColor, expectedColor, "badge color is correct"); + } + + + // TODO: Popup URL. + } + + let awaitFinish = new Promise(resolve => { + extension.onMessage("nextTest", (expecting, testsRemaining) => { + checkDetails(expecting); + + if (testsRemaining) { + extension.sendMessage("runNextTest"); + } else { + resolve(); + } + }); + }); + + yield extension.startup(); + + yield awaitFinish; + + yield extension.unload(); +} + +add_task(function* testTabSwitchContext() { + yield runTests({ + manifest: { + "browser_action": { + "default_icon": "default.png", + "default_popup": "__MSG_popup__", + "default_title": "Default __MSG_title__", + }, + + "default_locale": "en", + + "permissions": ["tabs"], + }, + + "files": { + "_locales/en/messages.json": { + "popup": { + "message": "default.html", + "description": "Popup", + }, + + "title": { + "message": "Title", + "description": "Title", + }, + }, + + "default.png": imageBuffer, + "default-2.png": imageBuffer, + "1.png": imageBuffer, + "2.png": imageBuffer, + }, + + getTests(tabs, expectDefaults) { + const DEFAULT_BADGE_COLOR = [0xd9, 0, 0, 255]; + + let details = [ + {"icon": browser.runtime.getURL("default.png"), + "popup": browser.runtime.getURL("default.html"), + "title": "Default Title", + "badge": "", + "badgeBackgroundColor": DEFAULT_BADGE_COLOR}, + {"icon": browser.runtime.getURL("1.png"), + "popup": browser.runtime.getURL("default.html"), + "title": "Default Title", + "badge": "", + "badgeBackgroundColor": DEFAULT_BADGE_COLOR}, + {"icon": browser.runtime.getURL("2.png"), + "popup": browser.runtime.getURL("2.html"), + "title": "Title 2", + "badge": "2", + "badgeBackgroundColor": [0xff, 0, 0, 0xff], + "disabled": true}, + {"icon": browser.runtime.getURL("1.png"), + "popup": browser.runtime.getURL("default-2.html"), + "title": "Default Title 2", + "badge": "d2", + "badgeBackgroundColor": [0, 0xff, 0, 0xff], + "disabled": true}, + {"icon": browser.runtime.getURL("1.png"), + "popup": browser.runtime.getURL("default-2.html"), + "title": "Default Title 2", + "badge": "d2", + "badgeBackgroundColor": [0, 0xff, 0, 0xff], + "disabled": false}, + {"icon": browser.runtime.getURL("default-2.png"), + "popup": browser.runtime.getURL("default-2.html"), + "title": "Default Title 2", + "badge": "d2", + "badgeBackgroundColor": [0, 0xff, 0, 0xff]}, + ]; + + return [ + async expect => { + browser.test.log("Initial state, expect default properties."); + + await expectDefaults(details[0]); + expect(details[0]); + }, + async expect => { + browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon."); + browser.browserAction.setIcon({tabId: tabs[0], path: "1.png"}); + + await expectDefaults(details[0]); + expect(details[1]); + }, + async expect => { + browser.test.log("Create a new tab. Expect default properties."); + let tab = await browser.tabs.create({active: true, url: "about:blank?0"}); + tabs.push(tab.id); + + await expectDefaults(details[0]); + expect(details[0]); + }, + async expect => { + browser.test.log("Change properties. Expect new properties."); + let tabId = tabs[1]; + browser.browserAction.setIcon({tabId, path: "2.png"}); + browser.browserAction.setPopup({tabId, popup: "2.html"}); + browser.browserAction.setTitle({tabId, title: "Title 2"}); + browser.browserAction.setBadgeText({tabId, text: "2"}); + browser.browserAction.setBadgeBackgroundColor({tabId, color: "#ff0000"}); + browser.browserAction.disable(tabId); + + await expectDefaults(details[0]); + expect(details[2]); + }, + expect => { + browser.test.log("Navigate to a new page. Expect no changes."); + + // TODO: This listener should not be necessary, but the |tabs.update| + // callback currently fires too early in e10s windows. + browser.tabs.onUpdated.addListener(function listener(tabId, changed) { + if (tabId == tabs[1] && changed.url) { + browser.tabs.onUpdated.removeListener(listener); + expect(details[2]); + } + }); + + browser.tabs.update(tabs[1], {url: "about:blank?1"}); + }, + async expect => { + browser.test.log("Switch back to the first tab. Expect previously set properties."); + await browser.tabs.update(tabs[0], {active: true}); + expect(details[1]); + }, + async expect => { + browser.test.log("Change default values, expect those changes reflected."); + browser.browserAction.setIcon({path: "default-2.png"}); + browser.browserAction.setPopup({popup: "default-2.html"}); + browser.browserAction.setTitle({title: "Default Title 2"}); + browser.browserAction.setBadgeText({text: "d2"}); + browser.browserAction.setBadgeBackgroundColor({color: [0, 0xff, 0, 0xff]}); + browser.browserAction.disable(); + + await expectDefaults(details[3]); + expect(details[3]); + }, + async expect => { + browser.test.log("Re-enable by default. Expect enabled."); + browser.browserAction.enable(); + + await expectDefaults(details[4]); + expect(details[4]); + }, + async expect => { + browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step."); + await browser.tabs.update(tabs[1], {active: true}); + + await expectDefaults(details[3]); + expect(details[2]); + }, + async expect => { + browser.test.log("Delete tab, switch back to tab 1. Expect previous results again."); + await browser.tabs.remove(tabs[1]); + expect(details[4]); + }, + async expect => { + browser.test.log("Create a new tab. Expect new default properties."); + let tab = await browser.tabs.create({active: true, url: "about:blank?2"}); + tabs.push(tab.id); + expect(details[5]); + }, + async expect => { + browser.test.log("Delete tab."); + await browser.tabs.remove(tabs[2]); + expect(details[4]); + }, + ]; + }, + }); +}); + +add_task(function* testDefaultTitle() { + yield runTests({ + manifest: { + "name": "Foo Extension", + + "browser_action": { + "default_icon": "icon.png", + }, + + "permissions": ["tabs"], + }, + + files: { + "icon.png": imageBuffer, + }, + + getTests(tabs, expectDefaults) { + const DEFAULT_BADGE_COLOR = [0xd9, 0, 0, 255]; + + let details = [ + {"title": "Foo Extension", + "popup": "", + "badge": "", + "badgeBackgroundColor": DEFAULT_BADGE_COLOR, + "icon": browser.runtime.getURL("icon.png")}, + {"title": "Foo Title", + "popup": "", + "badge": "", + "badgeBackgroundColor": DEFAULT_BADGE_COLOR, + "icon": browser.runtime.getURL("icon.png")}, + {"title": "Bar Title", + "popup": "", + "badge": "", + "badgeBackgroundColor": DEFAULT_BADGE_COLOR, + "icon": browser.runtime.getURL("icon.png")}, + {"title": "", + "popup": "", + "badge": "", + "badgeBackgroundColor": DEFAULT_BADGE_COLOR, + "icon": browser.runtime.getURL("icon.png")}, + ]; + + return [ + async expect => { + browser.test.log("Initial state. Expect extension title as default title."); + + await expectDefaults(details[0]); + expect(details[0]); + }, + async expect => { + browser.test.log("Change the title. Expect new title."); + browser.browserAction.setTitle({tabId: tabs[0], title: "Foo Title"}); + + await expectDefaults(details[0]); + expect(details[1]); + }, + async expect => { + browser.test.log("Change the default. Expect same properties."); + browser.browserAction.setTitle({title: "Bar Title"}); + + await expectDefaults(details[2]); + expect(details[1]); + }, + async expect => { + browser.test.log("Clear the title. Expect new default title."); + browser.browserAction.setTitle({tabId: tabs[0], title: ""}); + + await expectDefaults(details[2]); + expect(details[2]); + }, + async expect => { + browser.test.log("Set default title to null string. Expect null string from API, extension title in UI."); + browser.browserAction.setTitle({title: ""}); + + await expectDefaults(details[3]); + expect(details[3]); + }, + ]; + }, + }); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js b/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js new file mode 100644 index 000000000..c0b9c1a1d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js @@ -0,0 +1,68 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testDisabled() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": {}, + }, + + background: function() { + let clicked = false; + + browser.browserAction.onClicked.addListener(() => { + browser.test.log("Got click event"); + clicked = true; + }); + + browser.test.onMessage.addListener((msg, expectClick) => { + if (msg == "enable") { + browser.test.log("enable browserAction"); + browser.browserAction.enable(); + } else if (msg == "disable") { + browser.test.log("disable browserAction"); + browser.browserAction.disable(); + } else if (msg == "check-clicked") { + browser.test.assertEq(expectClick, clicked, "got click event?"); + clicked = false; + } else { + browser.test.fail("Unexpected message"); + } + + browser.test.sendMessage("next-test"); + }); + + browser.test.sendMessage("ready"); + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + yield clickBrowserAction(extension); + yield new Promise(resolve => setTimeout(resolve, 0)); + + extension.sendMessage("check-clicked", true); + yield extension.awaitMessage("next-test"); + + extension.sendMessage("disable"); + yield extension.awaitMessage("next-test"); + + yield clickBrowserAction(extension); + yield new Promise(resolve => setTimeout(resolve, 0)); + + extension.sendMessage("check-clicked", false); + yield extension.awaitMessage("next-test"); + + extension.sendMessage("enable"); + yield extension.awaitMessage("next-test"); + + yield clickBrowserAction(extension); + yield new Promise(resolve => setTimeout(resolve, 0)); + + extension.sendMessage("check-clicked", true); + yield extension.awaitMessage("next-test"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js new file mode 100644 index 000000000..9665d6832 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js @@ -0,0 +1,321 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Test that various combinations of icon details specs, for both paths +// and ImageData objects, result in the correct image being displayed in +// all display resolutions. +add_task(function* testDetailsObjects() { + function background() { + function getImageData(color) { + let canvas = document.createElement("canvas"); + canvas.width = 2; + canvas.height = 2; + let canvasContext = canvas.getContext("2d"); + + canvasContext.clearRect(0, 0, canvas.width, canvas.height); + canvasContext.fillStyle = color; + canvasContext.fillRect(0, 0, 1, 1); + + return { + url: canvas.toDataURL("image/png"), + imageData: canvasContext.getImageData(0, 0, canvas.width, canvas.height), + }; + } + + let imageData = { + red: getImageData("red"), + green: getImageData("green"), + }; + + /* eslint-disable comma-dangle, indent */ + let iconDetails = [ + // Only paths. + {details: {"path": "a.png"}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": browser.runtime.getURL("data/a.png")}}, + {details: {"path": "/a.png"}, + resolutions: { + "1": browser.runtime.getURL("a.png"), + "2": browser.runtime.getURL("a.png")}}, + {details: {"path": {"19": "a.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": browser.runtime.getURL("data/a.png")}}, + {details: {"path": {"38": "a.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": browser.runtime.getURL("data/a.png")}}, + {details: {"path": {"19": "a.png", "38": "a-x2.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": browser.runtime.getURL("data/a-x2.png")}}, + + // Test that CSS strings are escaped properly. + {details: {"path": 'a.png#" \\'}, + resolutions: { + "1": browser.runtime.getURL("data/a.png#%22%20%5C"), + "2": browser.runtime.getURL("data/a.png#%22%20%5C")}}, + + // Only ImageData objects. + {details: {"imageData": imageData.red.imageData}, + resolutions: { + "1": imageData.red.url, + "2": imageData.red.url}}, + {details: {"imageData": {"19": imageData.red.imageData}}, + resolutions: { + "1": imageData.red.url, + "2": imageData.red.url}}, + {details: {"imageData": {"38": imageData.red.imageData}}, + resolutions: { + "1": imageData.red.url, + "2": imageData.red.url}}, + {details: {"imageData": { + "19": imageData.red.imageData, + "38": imageData.green.imageData}}, + resolutions: { + "1": imageData.red.url, + "2": imageData.green.url}}, + + // Mixed path and imageData objects. + // + // The behavior is currently undefined if both |path| and + // |imageData| specify icons of the same size. + {details: { + "path": {"19": "a.png"}, + "imageData": {"38": imageData.red.imageData}}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": imageData.red.url}}, + {details: { + "path": {"38": "a.png"}, + "imageData": {"19": imageData.red.imageData}}, + resolutions: { + "1": imageData.red.url, + "2": browser.runtime.getURL("data/a.png")}}, + + // A path or ImageData object by itself is treated as a 19px icon. + {details: { + "path": "a.png", + "imageData": {"38": imageData.red.imageData}}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": imageData.red.url}}, + {details: { + "path": {"38": "a.png"}, + "imageData": imageData.red.imageData}, + resolutions: { + "1": imageData.red.url, + "2": browser.runtime.getURL("data/a.png")}}, + + // Various resolutions + {details: {"path": {"18": "a.png", "36": "a-x2.png"}}, + legacy: true, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": browser.runtime.getURL("data/a-x2.png")}}, + {details: {"path": {"16": "a.png", "30": "a-x2.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/a.png"), + "2": browser.runtime.getURL("data/a-x2.png")}}, + {details: {"path": {"16": "16.png", "100": "100.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/16.png"), + "2": browser.runtime.getURL("data/100.png")}}, + {details: {"path": {"2": "2.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/2.png"), + "2": browser.runtime.getURL("data/2.png")}}, + {details: {"path": { + "16": "16.svg", + "18": "18.svg"}}, + resolutions: { + "1": browser.runtime.getURL("data/16.svg"), + "2": browser.runtime.getURL("data/18.svg")}}, + {details: {"path": { + "6": "6.png", + "18": "18.png", + "36": "36.png", + "48": "48.png", + "128": "128.png"}}, + legacy: true, + resolutions: { + "1": browser.runtime.getURL("data/18.png"), + "2": browser.runtime.getURL("data/36.png")}, + menuResolutions: { + "1": browser.runtime.getURL("data/36.png"), + "2": browser.runtime.getURL("data/128.png")}}, + {details: {"path": { + "16": "16.png", + "18": "18.png", + "32": "32.png", + "48": "48.png", + "64": "64.png", + "128": "128.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/16.png"), + "2": browser.runtime.getURL("data/32.png")}, + menuResolutions: { + "1": browser.runtime.getURL("data/32.png"), + "2": browser.runtime.getURL("data/64.png")}}, + {details: {"path": { + "18": "18.png", + "32": "32.png", + "48": "48.png", + "128": "128.png"}}, + resolutions: { + "1": browser.runtime.getURL("data/32.png"), + "2": browser.runtime.getURL("data/32.png")}}, + ]; + + // Allow serializing ImageData objects for logging. + ImageData.prototype.toJSON = () => "<ImageData>"; + + let tabId; + + browser.test.onMessage.addListener((msg, test) => { + if (msg != "setIcon") { + browser.test.fail("expecting 'setIcon' message"); + } + + let details = iconDetails[test.index]; + + let detailString = JSON.stringify(details); + browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URLs ${JSON.stringify(details.resolutions)}`); + + Promise.all([ + browser.browserAction.setIcon(Object.assign({tabId}, details.details)), + browser.pageAction.setIcon(Object.assign({tabId}, details.details)), + ]).then(() => { + browser.test.sendMessage("iconSet"); + }); + }); + + // Generate a list of tests and resolutions to send back to the test + // context. + // + // This process is a bit convoluted, because the outer test context needs + // to handle checking the button nodes and changing the screen resolution, + // but it can't pass us icon definitions with ImageData objects. This + // shouldn't be a problem, since structured clones should handle ImageData + // objects without issue. Unfortunately, |cloneInto| implements a slightly + // different algorithm than we use in web APIs, and does not handle them + // correctly. + let tests = []; + for (let [idx, icon] of iconDetails.entries()) { + tests.push({ + index: idx, + legacy: !!icon.legacy, + menuResolutions: icon.menuResolutions, + resolutions: icon.resolutions, + }); + } + + // Sort by resolution, so we don't needlessly switch back and forth + // between each test. + tests.sort(test => test.resolution); + + browser.tabs.query({active: true, currentWindow: true}, tabs => { + tabId = tabs[0].id; + browser.pageAction.show(tabId).then(() => { + browser.test.sendMessage("ready", tests); + }); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": {}, + "page_action": {}, + "background": { + "page": "data/background.html", + } + }, + + files: { + "data/background.html": `<script src="background.js"></script>`, + "data/background.js": background, + + "data/16.svg": imageBuffer, + "data/18.svg": imageBuffer, + + "data/16.png": imageBuffer, + "data/18.png": imageBuffer, + "data/32.png": imageBuffer, + "data/36.png": imageBuffer, + "data/48.png": imageBuffer, + "data/64.png": imageBuffer, + "data/128.png": imageBuffer, + + "a.png": imageBuffer, + "data/2.png": imageBuffer, + "data/100.png": imageBuffer, + "data/a.png": imageBuffer, + "data/a-x2.png": imageBuffer, + }, + }); + + const RESOLUTION_PREF = "layout.css.devPixelsPerPx"; + + yield extension.startup(); + + let pageActionId = `${makeWidgetId(extension.id)}-page-action`; + let browserActionWidget = getBrowserActionWidget(extension); + + let tests = yield extension.awaitMessage("ready"); + for (let test of tests) { + extension.sendMessage("setIcon", test); + yield extension.awaitMessage("iconSet"); + + let browserActionButton = browserActionWidget.forWindow(window).node; + let pageActionImage = document.getElementById(pageActionId); + + + // Test icon sizes in the toolbar/urlbar. + for (let resolution of Object.keys(test.resolutions)) { + yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]}); + + is(window.devicePixelRatio, +resolution, "window has the required resolution"); + + let imageURL = test.resolutions[resolution]; + is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct image at ${resolution}x resolution`); + is(getListStyleImage(pageActionImage), imageURL, `page action has the correct image at ${resolution}x resolution`); + + let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon"); + is(isLegacy, test.legacy, "Legacy class should be present?"); + + yield SpecialPowers.popPrefEnv(); + } + + if (!test.menuResolutions) { + continue; + } + + + // Test icon sizes in the menu panel. + CustomizableUI.addWidgetToArea(browserActionWidget.id, + CustomizableUI.AREA_PANEL); + + yield showBrowserAction(extension); + browserActionButton = browserActionWidget.forWindow(window).node; + + for (let resolution of Object.keys(test.menuResolutions)) { + yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]}); + + is(window.devicePixelRatio, +resolution, "window has the required resolution"); + + let imageURL = test.menuResolutions[resolution]; + is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct menu image at ${resolution}x resolution`); + + yield SpecialPowers.popPrefEnv(); + } + + yield closeBrowserAction(extension); + + CustomizableUI.addWidgetToArea(browserActionWidget.id, + CustomizableUI.AREA_NAVBAR); + } + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js new file mode 100644 index 000000000..110746cae --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js @@ -0,0 +1,210 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Test that an error is thrown when providing invalid icon sizes +add_task(function* testInvalidIconSizes() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": {}, + "page_action": {}, + }, + + background: function() { + browser.tabs.query({active: true, currentWindow: true}, tabs => { + let tabId = tabs[0].id; + + let promises = []; + for (let api of ["pageAction", "browserAction"]) { + // helper function to run setIcon and check if it fails + let assertSetIconThrows = function(detail, error, message) { + detail.tabId = tabId; + promises.push( + browser.test.assertRejects( + browser[api].setIcon(detail), + /must be an integer/, + "setIcon with invalid icon size")); + }; + + let imageData = new ImageData(1, 1); + + // test invalid icon size inputs + for (let type of ["path", "imageData"]) { + let img = type == "imageData" ? imageData : "test.png"; + + assertSetIconThrows({[type]: {"abcdef": img}}); + assertSetIconThrows({[type]: {"48px": img}}); + assertSetIconThrows({[type]: {"20.5": img}}); + assertSetIconThrows({[type]: {"5.0": img}}); + assertSetIconThrows({[type]: {"-300": img}}); + assertSetIconThrows({[type]: {"abc": img, "5": img}}); + } + + assertSetIconThrows({imageData: {"abcdef": imageData}, path: {"5": "test.png"}}); + assertSetIconThrows({path: {"abcdef": "test.png"}, imageData: {"5": imageData}}); + } + + Promise.all(promises).then(() => { + browser.test.notifyPass("setIcon with invalid icon size"); + }); + }); + }, + }); + + yield Promise.all([extension.startup(), extension.awaitFinish("setIcon with invalid icon size")]); + + yield extension.unload(); +}); + + +// Test that default icon details in the manifest.json file are handled +// correctly. +add_task(function* testDefaultDetails() { + // TODO: Test localized variants. + let icons = [ + "foo/bar.png", + "/foo/bar.png", + {"19": "foo/bar.png"}, + {"38": "foo/bar.png"}, + ]; + + if (window.devicePixelRatio > 1) { + icons.push({"19": "baz/quux.png", "38": "foo/bar.png"}); + } else { + icons.push({"19": "foo/bar.png", "38": "baz/quux@2x.png"}); + } + + let expectedURL = new RegExp(String.raw`^moz-extension://[^/]+/foo/bar\.png$`); + + for (let icon of icons) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": {"default_icon": icon}, + "page_action": {"default_icon": icon}, + }, + + background: function() { + browser.tabs.query({active: true, currentWindow: true}, tabs => { + let tabId = tabs[0].id; + + browser.pageAction.show(tabId).then(() => { + browser.test.sendMessage("ready"); + }); + }); + }, + + files: { + "foo/bar.png": imageBuffer, + "baz/quux.png": imageBuffer, + "baz/quux@2x.png": imageBuffer, + }, + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let browserActionId = makeWidgetId(extension.id) + "-browser-action"; + let pageActionId = makeWidgetId(extension.id) + "-page-action"; + + let browserActionButton = document.getElementById(browserActionId); + let image = getListStyleImage(browserActionButton); + + ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`); + + let pageActionImage = document.getElementById(pageActionId); + image = getListStyleImage(pageActionImage); + + ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`); + + yield extension.unload(); + + let node = document.getElementById(pageActionId); + is(node, null, "pageAction image removed from document"); + } +}); + + +// Check that attempts to load a privileged URL as an icon image fail. +add_task(function* testSecureURLsDenied() { + // Test URLs passed to setIcon. + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": {}, + "page_action": {}, + }, + + background: function() { + browser.tabs.query({active: true, currentWindow: true}, tabs => { + let tabId = tabs[0].id; + + let urls = ["chrome://browser/content/browser.xul", + "javascript:true"]; + + let promises = []; + for (let url of urls) { + for (let api of ["pageAction", "browserAction"]) { + promises.push( + browser.test.assertRejects( + browser[api].setIcon({tabId, path: url}), + /Illegal URL/, + `Load of '${url}' should fail.`)); + } + } + + Promise.all(promises).then(() => { + browser.test.notifyPass("setIcon security tests"); + }); + }); + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("setIcon security tests"); + yield extension.unload(); +}); + + +add_task(function* testSecureManifestURLsDenied() { + // Test URLs included in the manifest. + + let urls = ["chrome://browser/content/browser.xul", + "javascript:true"]; + + let apis = ["browser_action", "page_action"]; + + for (let url of urls) { + for (let api of apis) { + info(`TEST ${api} icon url: ${url}`); + + let matchURLForbidden = url => ({ + message: new RegExp(`match the format "strictRelativeUrl"`), + }); + + let messages = [matchURLForbidden(url)]; + + let waitForConsole = new Promise(resolve => { + // Not necessary in browser-chrome tests, but monitorConsole gripes + // if we don't call it. + SimpleTest.waitForExplicitFinish(); + + SimpleTest.monitorConsole(resolve, messages); + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + [api]: { + "default_icon": url, + }, + }, + }); + + yield Assert.rejects(extension.startup(), + null, + "Manifest rejected"); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; + } + } +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js new file mode 100644 index 000000000..9f04b3c11 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js @@ -0,0 +1,413 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function getBrowserAction(extension) { + const {GlobalManager, Management: {global: {browserActionFor}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let ext = GlobalManager.extensionMap.get(extension.id); + return browserActionFor(ext); +} + +let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head><body>${url}</body></html>`; + +function* testInArea(area) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "background": { + "page": "data/background.html", + }, + "browser_action": { + "default_popup": "popup-a.html", + "browser_style": true, + }, + }, + + files: { + "popup-a.html": scriptPage("popup-a.js"), + "popup-a.js": function() { + window.onload = () => { + let color = window.getComputedStyle(document.body).color; + browser.test.assertEq("rgb(34, 36, 38)", color); + browser.runtime.sendMessage("from-popup-a"); + }; + browser.runtime.onMessage.addListener(msg => { + if (msg == "close-popup-using-window.close") { + window.close(); + } + }); + }, + + "data/popup-b.html": scriptPage("popup-b.js"), + "data/popup-b.js": function() { + window.onload = () => { + browser.runtime.sendMessage("from-popup-b"); + }; + }, + + "data/popup-c.html": scriptPage("popup-c.js"), + "data/popup-c.js": function() { + // Close the popup before the document is fully-loaded to make sure that + // we handle this case sanely. + browser.runtime.sendMessage("from-popup-c"); + window.close(); + }, + + "data/background.html": scriptPage("background.js"), + + "data/background.js": function() { + let sendClick; + let tests = [ + () => { + browser.test.log(`Click browser action, expect popup "a".`); + sendClick({expectEvent: false, expectPopup: "a"}); + }, + () => { + browser.test.log(`Click browser action again, expect popup "a".`); + sendClick({expectEvent: false, expectPopup: "a"}); + }, + () => { + browser.test.log(`Call triggerAction, expect popup "a" again. Leave popup open.`); + sendClick({expectEvent: false, expectPopup: "a", closePopup: false}, "trigger-action"); + }, + () => { + browser.test.log(`Call triggerAction again. Expect remaining popup closed.`); + sendClick({expectEvent: false, expectPopup: null}, "trigger-action"); + browser.test.sendMessage("next-test", {waitUntilClosed: true}); + }, + () => { + browser.test.log(`Call triggerAction again. Expect popup "a" again.`); + sendClick({expectEvent: false, expectPopup: "a"}, "trigger-action"); + }, + () => { + browser.test.log(`Set popup to "c" and click browser action. Expect popup "c".`); + browser.browserAction.setPopup({popup: "popup-c.html"}); + sendClick({expectEvent: false, expectPopup: "c", closePopup: false}); + }, + () => { + browser.test.log(`Set popup to "b" and click browser action. Expect popup "b".`); + browser.browserAction.setPopup({popup: "popup-b.html"}); + sendClick({expectEvent: false, expectPopup: "b"}); + }, + () => { + browser.test.log(`Click browser action again, expect popup "b".`); + sendClick({expectEvent: false, expectPopup: "b"}); + }, + () => { + browser.test.log(`Clear popup URL. Click browser action. Expect click event.`); + browser.browserAction.setPopup({popup: ""}); + sendClick({expectEvent: true, expectPopup: null}); + }, + () => { + browser.test.log(`Click browser action again. Expect another click event.`); + sendClick({expectEvent: true, expectPopup: null}); + }, + () => { + browser.test.log(`Call triggerAction. Expect click event.`); + sendClick({expectEvent: true, expectPopup: null}, "trigger-action"); + }, + () => { + browser.test.log(`Set popup to "a" and click browser action. Expect popup "a", and leave open.`); + browser.browserAction.setPopup({popup: "/popup-a.html"}); + sendClick({expectEvent: false, expectPopup: "a", closePopup: false}); + }, + () => { + browser.test.log(`Tell popup "a" to call window.close(). Expect popup closed.`); + browser.test.sendMessage("next-test", {closePopupUsingWindow: true}); + }, + ]; + + let expect = {}; + sendClick = ({expectEvent, expectPopup, runNextTest, waitUntilClosed, closePopup}, message = "send-click") => { + if (closePopup == undefined) { + closePopup = true; + } + + expect = {event: expectEvent, popup: expectPopup, runNextTest, waitUntilClosed, closePopup}; + browser.test.sendMessage(message); + }; + + browser.runtime.onMessage.addListener(msg => { + if (msg == "close-popup-using-window.close") { + return; + } else if (expect.popup) { + browser.test.assertEq(msg, `from-popup-${expect.popup}`, + "expected popup opened"); + } else { + browser.test.fail(`unexpected popup: ${msg}`); + } + + expect.popup = null; + browser.test.sendMessage("next-test", expect); + }); + + browser.browserAction.onClicked.addListener(() => { + if (expect.event) { + browser.test.succeed("expected click event received"); + } else { + browser.test.fail("unexpected click event"); + } + + expect.event = false; + browser.test.sendMessage("next-test", expect); + }); + + browser.test.onMessage.addListener((msg) => { + if (msg == "close-popup-using-window.close") { + browser.runtime.sendMessage("close-popup-using-window.close"); + return; + } + + if (msg != "next-test") { + browser.test.fail("Expecting 'next-test' message"); + } + + if (tests.length) { + let test = tests.shift(); + test(); + } else { + browser.test.notifyPass("browseraction-tests-done"); + } + }); + + browser.test.sendMessage("next-test"); + }, + }, + }); + + extension.onMessage("send-click", () => { + clickBrowserAction(extension); + }); + + extension.onMessage("trigger-action", () => { + getBrowserAction(extension).triggerAction(window); + }); + + let widget; + extension.onMessage("next-test", Task.async(function* (expecting = {}) { + if (!widget) { + widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, area); + } + if (expecting.waitUntilClosed) { + let panel = getBrowserActionPopup(extension); + if (panel && panel.state != "closed") { + yield promisePopupHidden(panel); + } + } else if (expecting.closePopupUsingWindow) { + let panel = getBrowserActionPopup(extension); + ok(panel, "Expect panel to exist"); + yield promisePopupShown(panel); + + extension.sendMessage("close-popup-using-window.close"); + + yield promisePopupHidden(panel); + ok(true, "Panel is closed"); + } else if (expecting.closePopup) { + yield closeBrowserAction(extension); + } + + extension.sendMessage("next-test"); + })); + + yield Promise.all([extension.startup(), extension.awaitFinish("browseraction-tests-done")]); + + yield extension.unload(); + + let view = document.getElementById(widget.viewId); + is(view, null, "browserAction view removed from document"); +} + +add_task(function* testBrowserActionInToolbar() { + yield testInArea(CustomizableUI.AREA_NAVBAR); +}); + +add_task(function* testBrowserActionInPanel() { + yield testInArea(CustomizableUI.AREA_PANEL); +}); + +add_task(function* testBrowserActionClickCanceled() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + "permissions": ["activeTab"], + }, + + files: { + "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head></html>`, + }, + }); + + yield extension.startup(); + + const {GlobalManager, Management: {global: {browserActionFor}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let ext = GlobalManager.extensionMap.get(extension.id); + let browserAction = browserActionFor(ext); + + let widget = getBrowserActionWidget(extension).forWindow(window); + let tab = window.gBrowser.selectedTab; + + // Test canceled click. + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, window); + + isnot(browserAction.pendingPopup, null, "Have pending popup"); + is(browserAction.pendingPopup.window, window, "Have pending popup for the correct window"); + + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + is(browserAction.tabToRevokeDuringClearPopup, tab, "Tab to revoke was saved"); + is(browserAction.tabManager.hasActiveTabPermission(tab), true, "Active tab was granted permission"); + + EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mouseup", button: 0}, window); + + is(browserAction.pendingPopup, null, "Pending popup was cleared"); + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + is(browserAction.tabToRevokeDuringClearPopup, null, "Tab to revoke was removed"); + is(browserAction.tabManager.hasActiveTabPermission(tab), false, "Permission was revoked from tab"); + + // Test completed click. + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, window); + + isnot(browserAction.pendingPopup, null, "Have pending popup"); + is(browserAction.pendingPopup.window, window, "Have pending popup for the correct window"); + + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + // We need to do these tests during the mouseup event cycle, since the click + // and command events will be dispatched immediately after mouseup, and void + // the results. + let mouseUpPromise = BrowserTestUtils.waitForEvent(widget.node, "mouseup", false, event => { + isnot(browserAction.pendingPopup, null, "Pending popup was not cleared"); + isnot(browserAction.pendingPopupTimeout, null, "Have a pending popup timeout"); + return true; + }); + + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseup", button: 0}, window); + + yield mouseUpPromise; + + is(browserAction.pendingPopup, null, "Pending popup was cleared"); + is(browserAction.pendingPopupTimeout, null, "Pending popup timeout was cleared"); + + yield promisePopupShown(getBrowserActionPopup(extension)); + yield closeBrowserAction(extension); + + yield extension.unload(); +}); + +add_task(function* testBrowserActionDisabled() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + }, + + background() { + browser.browserAction.disable(); + }, + + files: { + "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"><script src="popup.js"></script></head></html>`, + "popup.js"() { + browser.test.fail("Should not get here"); + }, + }, + }); + + yield extension.startup(); + + const {GlobalManager, Management: {global: {browserActionFor}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let ext = GlobalManager.extensionMap.get(extension.id); + let browserAction = browserActionFor(ext); + + let widget = getBrowserActionWidget(extension).forWindow(window); + + // Test canceled click. + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, window); + + is(browserAction.pendingPopup, null, "Have no pending popup"); + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mouseup", button: 0}, window); + + is(browserAction.pendingPopup, null, "Have no pending popup"); + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + + // Test completed click. + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, window); + + is(browserAction.pendingPopup, null, "Have no pending popup"); + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + // We need to do these tests during the mouseup event cycle, since the click + // and command events will be dispatched immediately after mouseup, and void + // the results. + let mouseUpPromise = BrowserTestUtils.waitForEvent(widget.node, "mouseup", false, event => { + is(browserAction.pendingPopup, null, "Have no pending popup"); + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + return true; + }); + + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseup", button: 0}, window); + + yield mouseUpPromise; + + is(browserAction.pendingPopup, null, "Have no pending popup"); + is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout"); + + // Give the popup a chance to load and trigger a failure, if it was + // erroneously opened. + yield new Promise(resolve => setTimeout(resolve, 250)); + + yield extension.unload(); +}); + +add_task(function* testBrowserActionTabPopulation() { + // Note: This test relates to https://bugzilla.mozilla.org/show_bug.cgi?id=1310019 + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + "permissions": ["activeTab"], + }, + + files: { + "popup.html": scriptPage("popup.js"), + "popup.js": function() { + browser.tabs.query({active: true, currentWindow: true}).then(tabs => { + browser.test.assertEq("mochitest index /", + tabs[0].title, + "Tab has the expected title on first click"); + browser.test.sendMessage("tabTitle"); + }); + }, + }, + }); + + let win = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "http://example.com/"); + yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + + yield extension.startup(); + + let widget = getBrowserActionWidget(extension).forWindow(win); + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, win); + + yield extension.awaitMessage("tabTitle"); + + EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseup", button: 0}, win); + + yield extension.unload(); + yield BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js new file mode 100644 index 000000000..6c19b17f1 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js @@ -0,0 +1,304 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* openPanel(extension, win = window, awaitLoad = false) { + clickBrowserAction(extension, win); + + return yield awaitExtensionPanel(extension, win, awaitLoad); +} + +add_task(function* testBrowserActionPopupResize() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + }, + + files: { + "popup.html": '<!DOCTYPE html><html><head><meta charset="utf-8"></head></html>', + }, + }); + + yield extension.startup(); + + let browser = yield openPanel(extension); + + function* checkSize(expected) { + let dims = yield promiseContentDimensions(browser); + + is(dims.window.innerHeight, expected, `Panel window should be ${expected}px tall`); + is(dims.body.clientHeight, dims.body.scrollHeight, + "Panel body should be tall enough to fit its contents"); + + // Tolerate if it is 1px too wide, as that may happen with the current resizing method. + ok(Math.abs(dims.window.innerWidth - expected) <= 1, `Panel window should be ${expected}px wide`); + is(dims.body.clientWidth, dims.body.scrollWidth, + "Panel body should be wide enough to fit its contents"); + } + + /* eslint-disable mozilla/no-cpows-in-tests */ + function setSize(size) { + content.document.body.style.height = `${size}px`; + content.document.body.style.width = `${size}px`; + } + /* eslint-enable mozilla/no-cpows-in-tests */ + + let sizes = [ + 200, + 400, + 300, + ]; + + for (let size of sizes) { + yield alterContent(browser, setSize, size); + yield checkSize(size); + } + + yield closeBrowserAction(extension); + yield extension.unload(); +}); + +function* testPopupSize(standardsMode, browserWin = window, arrowSide = "top") { + let docType = standardsMode ? "<!DOCTYPE html>" : ""; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + }, + + files: { + "popup.html": `${docType} + <html> + <head> + <meta charset="utf-8"> + <style type="text/css"> + body > span { + display: inline-block; + width: 10px; + height: 150px; + border: 2px solid black; + } + .big > span { + width: 300px; + height: 100px; + } + .bigger > span { + width: 150px; + height: 150px; + } + .huge > span { + height: ${2 * screen.height}px; + } + </style> + </head> + <body> + <span></span> + <span></span> + <span></span> + <span></span> + </body> + </html>`, + }, + }); + + yield extension.startup(); + + /* eslint-disable mozilla/no-cpows-in-tests */ + + if (arrowSide == "top") { + // Test the standalone panel for a toolbar button. + let browser = yield openPanel(extension, browserWin, true); + + let dims = yield promiseContentDimensions(browser); + + is(dims.isStandards, standardsMode, "Document has the expected compat mode"); + + let {innerWidth, innerHeight} = dims.window; + + dims = yield alterContent(browser, () => { + content.document.body.classList.add("bigger"); + }); + + let win = dims.window; + is(win.innerHeight, innerHeight, "Window height should not change"); + ok(win.innerWidth > innerWidth, `Window width should increase (${win.innerWidth} > ${innerWidth})`); + + + dims = yield alterContent(browser, () => { + content.document.body.classList.remove("bigger"); + }); + + win = dims.window; + is(win.innerHeight, innerHeight, "Window height should not change"); + + // The getContentSize calculation is not always reliable to single-pixel + // precision. + ok(Math.abs(win.innerWidth - innerWidth) <= 1, + `Window width should return to approximately its original value (${win.innerWidth} ~= ${innerWidth})`); + + yield closeBrowserAction(extension, browserWin); + } + + + // Test the PanelUI panel for a menu panel button. + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL); + + let browser = yield openPanel(extension, browserWin); + + let {panel} = browserWin.PanelUI; + let origPanelRect = panel.getBoundingClientRect(); + + // Check that the panel is still positioned as expected. + let checkPanelPosition = () => { + is(panel.getAttribute("side"), arrowSide, "Panel arrow is positioned as expected"); + + let panelRect = panel.getBoundingClientRect(); + if (arrowSide == "top") { + ok(panelRect.top, origPanelRect.top, "Panel has not moved downwards"); + ok(panelRect.bottom >= origPanelRect.bottom, `Panel has not shrunk from original size (${panelRect.bottom} >= ${origPanelRect.bottom})`); + + let screenBottom = browserWin.screen.availTop + browserWin.screen.availHeight; + let panelBottom = browserWin.mozInnerScreenY + panelRect.bottom; + ok(panelBottom <= screenBottom, `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`); + } else { + ok(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards"); + ok(panelRect.top <= origPanelRect.top, `Panel has not shrunk from original size (${panelRect.top} <= ${origPanelRect.top})`); + + let panelTop = browserWin.mozInnerScreenY + panelRect.top; + ok(panelTop >= browserWin.screen.availTop, `Top of popup should be on-screen. (${panelTop} >= ${browserWin.screen.availTop})`); + } + }; + + yield awaitBrowserLoaded(browser); + + // Wait long enough to make sure the initial resize debouncing timer has + // expired. + yield new Promise(resolve => setTimeout(resolve, 100)); + + let dims = yield promiseContentDimensions(browser); + + is(dims.isStandards, standardsMode, "Document has the expected compat mode"); + + // If the browser's preferred height is smaller than the initial height of the + // panel, then it will still take up the full available vertical space. Even + // so, we need to check that we've gotten the preferred height calculation + // correct, so check that explicitly. + let getHeight = () => parseFloat(browser.style.height); + + let {innerWidth, innerHeight} = dims.window; + let height = getHeight(); + + + let setClass = className => { + content.document.body.className = className; + }; + + info("Increase body children's width. " + + "Expect them to wrap, and the frame to grow vertically rather than widen."); + + dims = yield alterContent(browser, setClass, "big"); + let win = dims.window; + + ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`); + + is(win.innerWidth, innerWidth, "Window width should not change"); + ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`); + is(win.scrollMaxY, 0, "Document should not be vertically scrollable"); + + checkPanelPosition(); + + + info("Increase body children's width and height. " + + "Expect them to wrap, and the frame to grow vertically rather than widen."); + + dims = yield alterContent(browser, setClass, "bigger"); + win = dims.window; + + ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`); + + is(win.innerWidth, innerWidth, "Window width should not change"); + ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`); + is(win.scrollMaxY, 0, "Document should not be vertically scrollable"); + + checkPanelPosition(); + + + info("Increase body height beyond the height of the screen. " + + "Expect the panel to grow to accommodate, but not larger than the height of the screen."); + + dims = yield alterContent(browser, setClass, "huge"); + win = dims.window; + + ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`); + + is(win.innerWidth, innerWidth, "Window width should not change"); + ok(win.innerHeight > innerHeight, `Window height should increase (${win.innerHeight} > ${innerHeight})`); + ok(win.innerHeight < screen.height, `Window height be less than the screen height (${win.innerHeight} < ${screen.height})`); + ok(win.scrollMaxY > 0, `Document should be vertically scrollable (${win.scrollMaxY} > 0)`); + + checkPanelPosition(); + + + info("Restore original styling. Expect original dimensions."); + dims = yield alterContent(browser, setClass, ""); + win = dims.window; + + is(getHeight(), height, "Browser height should return to its original value"); + + is(win.innerWidth, innerWidth, "Window width should not change"); + is(win.innerHeight, innerHeight, "Window height should return to its original value"); + is(win.scrollMaxY, 0, "Document should not be vertically scrollable"); + + checkPanelPosition(); + + yield closeBrowserAction(extension, browserWin); + + yield extension.unload(); +} + +add_task(function* testBrowserActionMenuResizeStandards() { + yield testPopupSize(true); +}); + +add_task(function* testBrowserActionMenuResizeQuirks() { + yield testPopupSize(false); +}); + +// Test that we still make reasonable maximum size calculations when the window +// is close enough to the bottom of the screen that the menu panel opens above, +// rather than below, its button. +add_task(function* testBrowserActionMenuResizeBottomArrow() { + const WIDTH = 800; + const HEIGHT = 300; + + let left = screen.availLeft + screen.availWidth - WIDTH; + let top = screen.availTop + screen.availHeight - HEIGHT; + + let win = yield BrowserTestUtils.openNewBrowserWindow(); + + win.resizeTo(WIDTH, HEIGHT); + + // Sometimes we run into problems on Linux with resizing being asynchronous + // and window managers not allowing us to move the window so that any part of + // it is off-screen, so we need to try more than once. + for (let i = 0; i < 20; i++) { + win.moveTo(left, top); + + if (win.screenX == left && win.screenY == top) { + break; + } + + yield new Promise(resolve => setTimeout(resolve, 100)); + } + + yield testPopupSize(true, win, "bottom"); + + yield BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js b/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js new file mode 100644 index 000000000..e83010958 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js @@ -0,0 +1,59 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "unrecognized_property": "with-a-random-value", + }, + }, + + files: { + "popup.html": ` + <!DOCTYPE html> + <html><body> + <script src="popup.js"></script> + </body></html> + `, + + "popup.js": function() { + window.onload = () => { + browser.runtime.sendMessage("from-popup"); + }; + }, + }, + + background: function() { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(msg, "from-popup", "correct message received"); + browser.test.sendMessage("popup"); + }); + }, + }); + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: /Reading manifest: Error processing browser_action.unrecognized_property: An unexpected property was found/, + }]); + }); + + yield extension.startup(); + + // Do this a few times to make sure the pop-up is reloaded each time. + for (let i = 0; i < 3; i++) { + clickBrowserAction(extension); + + yield extension.awaitMessage("popup"); + + closeBrowserAction(extension); + } + + yield extension.unload(); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; +}); diff --git a/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js new file mode 100644 index 000000000..f97a735d4 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js @@ -0,0 +1,113 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* testExecuteBrowserActionWithOptions(options = {}) { + let extensionOptions = {}; + + extensionOptions.manifest = { + "commands": { + "_execute_browser_action": { + "suggested_key": { + "default": "Alt+Shift+J", + }, + }, + }, + "browser_action": { + "browser_style": true, + }, + }; + + if (options.withPopup) { + extensionOptions.manifest.browser_action.default_popup = "popup.html"; + + extensionOptions.files = { + "popup.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="popup.js"></script> + </head> + </html> + `, + + "popup.js": function() { + browser.runtime.sendMessage("from-browser-action-popup"); + }, + }; + } + + extensionOptions.background = () => { + browser.test.onMessage.addListener((message, withPopup) => { + browser.commands.onCommand.addListener((commandName) => { + if (commandName == "_execute_browser_action") { + browser.test.fail("The onCommand listener should never fire for _execute_browser_action."); + } + }); + + browser.browserAction.onClicked.addListener(() => { + if (withPopup) { + browser.test.fail("The onClick listener should never fire if the browserAction has a popup."); + browser.test.notifyFail("execute-browser-action-on-clicked-fired"); + } else { + browser.test.notifyPass("execute-browser-action-on-clicked-fired"); + } + }); + + browser.runtime.onMessage.addListener(msg => { + if (msg == "from-browser-action-popup") { + browser.test.notifyPass("execute-browser-action-popup-opened"); + } + }); + + browser.test.sendMessage("send-keys"); + }); + }; + + let extension = ExtensionTestUtils.loadExtension(extensionOptions); + + extension.onMessage("send-keys", () => { + EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true}); + }); + + yield extension.startup(); + + if (options.inArea) { + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, options.inArea); + } + + extension.sendMessage("withPopup", options.withPopup); + + if (options.withPopup) { + yield extension.awaitFinish("execute-browser-action-popup-opened"); + yield closeBrowserAction(extension); + } else { + yield extension.awaitFinish("execute-browser-action-on-clicked-fired"); + } + yield extension.unload(); +} + +add_task(function* test_execute_browser_action_with_popup() { + yield testExecuteBrowserActionWithOptions({ + withPopup: true, + }); +}); + +add_task(function* test_execute_browser_action_without_popup() { + yield testExecuteBrowserActionWithOptions(); +}); + +add_task(function* test_execute_browser_action_in_hamburger_menu_with_popup() { + yield testExecuteBrowserActionWithOptions({ + withPopup: true, + inArea: CustomizableUI.AREA_PANEL, + }); +}); + +add_task(function* test_execute_browser_action_in_hamburger_menu_without_popup() { + yield testExecuteBrowserActionWithOptions({ + inArea: CustomizableUI.AREA_PANEL, + }); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js b/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js new file mode 100644 index 000000000..83684493e --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js @@ -0,0 +1,133 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_execute_page_action_without_popup() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "commands": { + "_execute_page_action": { + "suggested_key": { + "default": "Alt+Shift+J", + }, + }, + "send-keys-command": { + "suggested_key": { + "default": "Alt+Shift+3", + }, + }, + }, + "page_action": {}, + }, + + background: function() { + let isShown = false; + + browser.commands.onCommand.addListener((commandName) => { + if (commandName == "_execute_page_action") { + browser.test.fail(`The onCommand listener should never fire for ${commandName}.`); + } else if (commandName == "send-keys-command") { + if (!isShown) { + isShown = true; + browser.tabs.query({currentWindow: true, active: true}, tabs => { + tabs.forEach(tab => { + browser.pageAction.show(tab.id); + }); + browser.test.sendMessage("send-keys"); + }); + } + } + }); + + browser.pageAction.onClicked.addListener(() => { + browser.test.assertTrue(isShown, "The onClicked event should fire if the page action is shown."); + browser.test.notifyPass("page-action-without-popup"); + }); + + browser.test.sendMessage("send-keys"); + }, + }); + + extension.onMessage("send-keys", () => { + EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true}); + EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true}); + }); + + yield extension.startup(); + yield extension.awaitFinish("page-action-without-popup"); + yield extension.unload(); +}); + +add_task(function* test_execute_page_action_with_popup() { + let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head><body>Test Popup</body></html>`; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "commands": { + "_execute_page_action": { + "suggested_key": { + "default": "Alt+Shift+J", + }, + }, + "send-keys-command": { + "suggested_key": { + "default": "Alt+Shift+3", + }, + }, + }, + "page_action": { + "default_popup": "popup.html", + }, + }, + + files: { + "popup.html": scriptPage("popup.js"), + "popup.js": function() { + browser.runtime.sendMessage("popup-opened"); + }, + }, + + background: function() { + let isShown = false; + + browser.commands.onCommand.addListener((message) => { + if (message == "_execute_page_action") { + browser.test.fail(`The onCommand listener should never fire for ${message}.`); + } + + if (message == "send-keys-command") { + if (!isShown) { + isShown = true; + browser.tabs.query({currentWindow: true, active: true}, tabs => { + tabs.forEach(tab => { + browser.pageAction.show(tab.id); + }); + browser.test.sendMessage("send-keys"); + }); + } + } + }); + + browser.pageAction.onClicked.addListener(() => { + browser.test.fail(`The onClicked listener should never fire when the pageAction has a popup.`); + }); + + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(msg, "popup-opened", "expected popup opened"); + browser.test.assertTrue(isShown, "The onClicked event should fire if the page action is shown."); + browser.test.notifyPass("page-action-with-popup"); + }); + + browser.test.sendMessage("send-keys"); + }, + }); + + extension.onMessage("send-keys", () => { + EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true}); + EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true}); + }); + + yield extension.startup(); + yield extension.awaitFinish("page-action-with-popup"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_commands_getAll.js b/browser/components/extensions/test/browser/browser_ext_commands_getAll.js new file mode 100644 index 000000000..5885e8aee --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_commands_getAll.js @@ -0,0 +1,81 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm"); + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "name": "Commands Extension", + "commands": { + "with-desciption": { + "suggested_key": { + "default": "Ctrl+Shift+Y", + }, + "description": "should have a description", + }, + "without-description": { + "suggested_key": { + "default": "Ctrl+Shift+D", + }, + }, + "with-platform-info": { + "suggested_key": { + "mac": "Ctrl+Shift+M", + "linux": "Ctrl+Shift+L", + "windows": "Ctrl+Shift+W", + "android": "Ctrl+Shift+A", + }, + }, + }, + }, + + background: function() { + browser.test.onMessage.addListener((message, additionalScope) => { + browser.commands.getAll((commands) => { + let errorMessage = "getAll should return an array of commands"; + browser.test.assertEq(commands.length, 3, errorMessage); + + let command = commands.find(c => c.name == "with-desciption"); + + errorMessage = "The description should match what is provided in the manifest"; + browser.test.assertEq("should have a description", command.description, errorMessage); + + errorMessage = "The shortcut should match the default shortcut provided in the manifest"; + browser.test.assertEq("Ctrl+Shift+Y", command.shortcut, errorMessage); + + command = commands.find(c => c.name == "without-description"); + + errorMessage = "The description should be empty when it is not provided"; + browser.test.assertEq(null, command.description, errorMessage); + + errorMessage = "The shortcut should match the default shortcut provided in the manifest"; + browser.test.assertEq("Ctrl+Shift+D", command.shortcut, errorMessage); + + let platformKeys = { + macosx: "M", + linux: "L", + win: "W", + android: "A", + }; + + command = commands.find(c => c.name == "with-platform-info"); + let platformKey = platformKeys[additionalScope.platform]; + let shortcut = `Ctrl+Shift+${platformKey}`; + errorMessage = `The shortcut should match the one provided in the manifest for OS='${additionalScope.platform}'`; + browser.test.assertEq(shortcut, command.shortcut, errorMessage); + + browser.test.notifyPass("commands"); + }); + }); + browser.test.sendMessage("ready"); + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + extension.sendMessage("additional-scope", {platform: AppConstants.platform}); + yield extension.awaitFinish("commands"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js new file mode 100644 index 000000000..dd959dcec --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js @@ -0,0 +1,229 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://gre/modules/AppConstants.jsm"); + +add_task(function* test_user_defined_commands() { + const testCommands = [ + // Ctrl Shortcuts + { + name: "toggle-ctrl-a", + shortcut: "Ctrl+A", + key: "A", + modifiers: { + accelKey: true, + }, + }, + { + name: "toggle-ctrl-up", + shortcut: "Ctrl+Up", + key: "VK_UP", + modifiers: { + accelKey: true, + }, + }, + // Alt Shortcuts + { + name: "toggle-alt-a", + shortcut: "Alt+A", + key: "A", + modifiers: { + altKey: true, + }, + }, + { + name: "toggle-alt-down", + shortcut: "Alt+Down", + key: "VK_DOWN", + modifiers: { + altKey: true, + }, + }, + // Mac Shortcuts + { + name: "toggle-command-shift-page-up", + shortcutMac: "Command+Shift+PageUp", + key: "VK_PAGE_UP", + modifiers: { + accelKey: true, + shiftKey: true, + }, + }, + { + name: "toggle-mac-control-shift+period", + shortcut: "Ctrl+Shift+Period", + shortcutMac: "MacCtrl+Shift+Period", + key: "VK_PERIOD", + modifiers: { + ctrlKey: true, + shiftKey: true, + }, + }, + // Ctrl+Shift Shortcuts + { + name: "toggle-ctrl-shift-left", + shortcut: "Ctrl+Shift+Left", + key: "VK_LEFT", + modifiers: { + accelKey: true, + shiftKey: true, + }, + }, + // Alt+Shift Shortcuts + { + name: "toggle-alt-shift-1", + shortcut: "Alt+Shift+1", + key: "1", + modifiers: { + altKey: true, + shiftKey: true, + }, + }, + { + name: "toggle-alt-shift-a", + shortcut: "Alt+Shift+A", + key: "A", + modifiers: { + altKey: true, + shiftKey: true, + }, + }, + { + name: "toggle-alt-shift-right", + shortcut: "Alt+Shift+Right", + key: "VK_RIGHT", + modifiers: { + altKey: true, + shiftKey: true, + }, + }, + // Misc Shortcuts + { + name: "valid-command-with-unrecognized-property-name", + shortcut: "Alt+Shift+3", + key: "3", + modifiers: { + altKey: true, + shiftKey: true, + }, + unrecognized_property: "with-a-random-value", + }, + { + name: "spaces-in-shortcut-name", + shortcut: " Alt + Shift + 2 ", + key: "2", + modifiers: { + altKey: true, + shiftKey: true, + }, + }, + ]; + + // Create a window before the extension is loaded. + let win1 = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.loadURI(win1.gBrowser.selectedBrowser, "about:robots"); + yield BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser); + + let commands = {}; + let isMac = AppConstants.platform == "macosx"; + let totalMacOnlyCommands = 0; + + for (let testCommand of testCommands) { + let command = { + suggested_key: {}, + }; + + if (testCommand.shortcut) { + command.suggested_key.default = testCommand.shortcut; + } + + if (testCommand.shortcutMac) { + command.suggested_key.mac = testCommand.shortcutMac; + } + + if (testCommand.shortcutMac && !testCommand.shortcut) { + totalMacOnlyCommands++; + } + + if (testCommand.unrecognized_property) { + command.unrecognized_property = testCommand.unrecognized_property; + } + + commands[testCommand.name] = command; + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "commands": commands, + }, + + background: function() { + browser.commands.onCommand.addListener(commandName => { + browser.test.sendMessage("oncommand", commandName); + }); + browser.test.sendMessage("ready"); + }, + }); + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: /Reading manifest: Error processing commands.*.unrecognized_property: An unexpected property was found/, + }]); + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + function* runTest(window) { + for (let testCommand of testCommands) { + if (testCommand.shortcutMac && !testCommand.shortcut && !isMac) { + continue; + } + EventUtils.synthesizeKey(testCommand.key, testCommand.modifiers, window); + let message = yield extension.awaitMessage("oncommand"); + is(message, testCommand.name, `Expected onCommand listener to fire with the correct name: ${testCommand.name}`); + } + } + + // Create another window after the extension is loaded. + let win2 = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.loadURI(win2.gBrowser.selectedBrowser, "about:robots"); + yield BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser); + + let totalTestCommands = Object.keys(testCommands).length; + let expectedCommandsRegistered = isMac ? totalTestCommands : totalTestCommands - totalMacOnlyCommands; + + // Confirm the keysets have been added to both windows. + let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`; + let keyset = win1.document.getElementById(keysetID); + ok(keyset != null, "Expected keyset to exist"); + is(keyset.childNodes.length, expectedCommandsRegistered, "Expected keyset to have the correct number of children"); + + keyset = win2.document.getElementById(keysetID); + ok(keyset != null, "Expected keyset to exist"); + is(keyset.childNodes.length, expectedCommandsRegistered, "Expected keyset to have the correct number of children"); + + // Confirm that the commands are registered to both windows. + yield focusWindow(win1); + yield runTest(win1); + + yield focusWindow(win2); + yield runTest(win2); + + yield extension.unload(); + + // Confirm that the keysets have been removed from both windows after the extension is unloaded. + keyset = win1.document.getElementById(keysetID); + is(keyset, null, "Expected keyset to be removed from the window"); + + keyset = win2.document.getElementById(keysetID); + is(keyset, null, "Expected keyset to be removed from the window"); + + yield BrowserTestUtils.closeWindow(win1); + yield BrowserTestUtils.closeWindow(win2); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js b/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js new file mode 100644 index 000000000..8b2d9badf --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js @@ -0,0 +1,67 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/"], + }, + + background: function() { + let ports_received = 0; + let port_messages_received = 0; + + browser.runtime.onConnect.addListener((port) => { + browser.test.assertTrue(!!port, "port1 received"); + + ports_received++; + browser.test.assertEq(1, ports_received, "1 port received"); + + port.onMessage.addListener((msg, msgPort) => { + browser.test.assertEq("port message", msg, "listener1 port message received"); + browser.test.assertEq(port, msgPort, "onMessage should receive port as second argument"); + + port_messages_received++; + browser.test.assertEq(1, port_messages_received, "1 port message received"); + }); + }); + browser.runtime.onConnect.addListener((port) => { + browser.test.assertTrue(!!port, "port2 received"); + + ports_received++; + browser.test.assertEq(2, ports_received, "2 ports received"); + + port.onMessage.addListener((msg, msgPort) => { + browser.test.assertEq("port message", msg, "listener2 port message received"); + browser.test.assertEq(port, msgPort, "onMessage should receive port as second argument"); + + port_messages_received++; + browser.test.assertEq(2, port_messages_received, "2 port messages received"); + + browser.test.notifyPass("contentscript_connect.pass"); + }); + }); + + browser.tabs.executeScript({file: "script.js"}).catch(e => { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("contentscript_connect.pass"); + }); + }, + + files: { + "script.js": function() { + let port = browser.runtime.connect(); + port.postMessage("port message"); + }, + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("contentscript_connect.pass"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus.js b/browser/components/extensions/test/browser/browser_ext_contextMenus.js new file mode 100644 index 000000000..fa1483b20 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js @@ -0,0 +1,342 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const PAGE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + + background: function() { + browser.contextMenus.create({ + id: "clickme-image", + title: "Click me!", + contexts: ["image"], + }); + browser.contextMenus.create({ + id: "clickme-page", + title: "Click me!", + contexts: ["page"], + }); + browser.test.notifyPass(); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish(); + + let contentAreaContextMenu = yield openContextMenu("#img1"); + let item = contentAreaContextMenu.getElementsByAttribute("label", "Click me!"); + is(item.length, 1, "contextMenu item for image was found"); + yield closeContextMenu(); + + contentAreaContextMenu = yield openContextMenu("body"); + item = contentAreaContextMenu.getElementsByAttribute("label", "Click me!"); + is(item.length, 1, "contextMenu item for page was found"); + yield closeContextMenu(); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); +}); + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + + background: async function() { + // A generic onclick callback function. + function genericOnClick(info, tab) { + browser.test.sendMessage("onclick", {info, tab}); + } + + browser.contextMenus.onClicked.addListener((info, tab) => { + browser.test.sendMessage("browser.contextMenus.onClicked", {info, tab}); + }); + + browser.contextMenus.create({ + contexts: ["all"], + type: "separator", + }); + + let contexts = ["page", "selection", "image", "editable"]; + for (let i = 0; i < contexts.length; i++) { + let context = contexts[i]; + let title = context; + browser.contextMenus.create({ + title: title, + contexts: [context], + id: "ext-" + context, + onclick: genericOnClick, + }); + if (context == "selection") { + browser.contextMenus.update("ext-selection", { + title: "selection is: '%s'", + onclick: (info, tab) => { + browser.contextMenus.removeAll(); + genericOnClick(info, tab); + }, + }); + } + } + + let parent = browser.contextMenus.create({ + title: "parent", + }); + browser.contextMenus.create({ + title: "child1", + parentId: parent, + onclick: genericOnClick, + }); + let child2 = browser.contextMenus.create({ + title: "child2", + parentId: parent, + onclick: genericOnClick, + }); + + let parentToDel = browser.contextMenus.create({ + title: "parentToDel", + }); + browser.contextMenus.create({ + title: "child1", + parentId: parentToDel, + onclick: genericOnClick, + }); + browser.contextMenus.create({ + title: "child2", + parentId: parentToDel, + onclick: genericOnClick, + }); + browser.contextMenus.remove(parentToDel); + + browser.contextMenus.create({ + title: "Without onclick property", + id: "ext-without-onclick", + }); + + await browser.test.assertRejects( + browser.contextMenus.update(parent, {parentId: child2}), + /cannot be an ancestor/, + "Should not be able to reparent an item as descendent of itself"); + + browser.test.notifyPass("contextmenus"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("contextmenus"); + + let expectedClickInfo = { + menuItemId: "ext-image", + mediaType: "image", + srcUrl: "http://mochi.test:8888/browser/browser/components/extensions/test/browser/ctxmenu-image.png", + pageUrl: PAGE, + editable: false, + }; + + function checkClickInfo(result) { + for (let i of Object.keys(expectedClickInfo)) { + is(result.info[i], expectedClickInfo[i], + "click info " + i + " expected to be: " + expectedClickInfo[i] + " but was: " + result.info[i]); + } + is(expectedClickInfo.pageSrc, result.tab.url, "click info page source is the right tab"); + } + + let extensionMenuRoot = yield openExtensionContextMenu(); + + // Check some menu items + let items = extensionMenuRoot.getElementsByAttribute("label", "image"); + is(items.length, 1, "contextMenu item for image was found (context=image)"); + let image = items[0]; + + items = extensionMenuRoot.getElementsByAttribute("label", "selection-edited"); + is(items.length, 0, "contextMenu item for selection was not found (context=image)"); + + items = extensionMenuRoot.getElementsByAttribute("label", "parentToDel"); + is(items.length, 0, "contextMenu item for removed parent was not found (context=image)"); + + items = extensionMenuRoot.getElementsByAttribute("label", "parent"); + is(items.length, 1, "contextMenu item for parent was found (context=image)"); + + is(items[0].childNodes[0].childNodes.length, 2, "child items for parent were found (context=image)"); + + // Click on ext-image item and check the click results + yield closeExtensionContextMenu(image); + + let result = yield extension.awaitMessage("onclick"); + checkClickInfo(result); + result = yield extension.awaitMessage("browser.contextMenus.onClicked"); + checkClickInfo(result); + + + // Test "editable" context and OnClick data property. + extensionMenuRoot = yield openExtensionContextMenu("#edit-me"); + + // Check some menu items. + items = extensionMenuRoot.getElementsByAttribute("label", "editable"); + is(items.length, 1, "contextMenu item for text input element was found (context=editable)"); + let editable = items[0]; + + // Click on ext-editable item and check the click results. + yield closeExtensionContextMenu(editable); + + expectedClickInfo = { + menuItemId: "ext-editable", + pageUrl: PAGE, + editable: true, + }; + + result = yield extension.awaitMessage("onclick"); + checkClickInfo(result); + result = yield extension.awaitMessage("browser.contextMenus.onClicked"); + checkClickInfo(result); + + + // Select some text + yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* (arg) { + let doc = content.document; + let range = doc.createRange(); + let selection = content.getSelection(); + selection.removeAllRanges(); + let textNode = doc.getElementById("img1").previousSibling; + range.setStart(textNode, 0); + range.setEnd(textNode, 100); + selection.addRange(range); + }); + + // Bring up context menu again + extensionMenuRoot = yield openExtensionContextMenu(); + + // Check some menu items + items = extensionMenuRoot.getElementsByAttribute("label", "Without onclick property"); + is(items.length, 1, "contextMenu item was found (context=page)"); + + yield closeExtensionContextMenu(items[0]); + + expectedClickInfo = { + menuItemId: "ext-without-onclick", + pageUrl: PAGE, + }; + + result = yield extension.awaitMessage("browser.contextMenus.onClicked"); + checkClickInfo(result); + + // Bring up context menu again + extensionMenuRoot = yield openExtensionContextMenu(); + + // Check some menu items + items = extensionMenuRoot.getElementsByAttribute("label", "selection is: 'just some text 123456789012345678901234567890...'"); + is(items.length, 1, "contextMenu item for selection was found (context=selection)"); + let selectionItem = items[0]; + + items = extensionMenuRoot.getElementsByAttribute("label", "selection"); + is(items.length, 0, "contextMenu item label update worked (context=selection)"); + + yield closeExtensionContextMenu(selectionItem); + + expectedClickInfo = { + menuItemId: "ext-selection", + pageUrl: PAGE, + selectionText: "just some text 1234567890123456789012345678901234567890123456789012345678901234567890123456789012", + }; + + result = yield extension.awaitMessage("onclick"); + checkClickInfo(result); + result = yield extension.awaitMessage("browser.contextMenus.onClicked"); + checkClickInfo(result); + + let contentAreaContextMenu = yield openContextMenu("#img1"); + items = contentAreaContextMenu.getElementsByAttribute("ext-type", "top-level-menu"); + is(items.length, 0, "top level item was not found (after removeAll()"); + yield closeContextMenu(); + + yield extension.unload(); + yield BrowserTestUtils.removeTab(tab1); +}); + +add_task(function* testRemoveAllWithTwoExtensions() { + const tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + const manifest = {permissions: ["contextMenus"]}; + + const first = ExtensionTestUtils.loadExtension({manifest, background() { + browser.contextMenus.create({title: "alpha", contexts: ["all"]}); + + browser.contextMenus.onClicked.addListener(() => { + browser.contextMenus.removeAll(); + }); + browser.test.onMessage.addListener(msg => { + if (msg == "ping") { + browser.test.sendMessage("pong-alpha"); + return; + } + browser.contextMenus.create({title: "gamma", contexts: ["all"]}); + }); + }}); + + const second = ExtensionTestUtils.loadExtension({manifest, background() { + browser.contextMenus.create({title: "beta", contexts: ["all"]}); + + browser.contextMenus.onClicked.addListener(() => { + browser.contextMenus.removeAll(); + }); + + browser.test.onMessage.addListener(() => { + browser.test.sendMessage("pong-beta"); + }); + }}); + + yield first.startup(); + yield second.startup(); + + function* confirmMenuItems(...items) { + // Round-trip to extension to make sure that the context menu state has been + // updated by the async contextMenus.create / contextMenus.removeAll calls. + first.sendMessage("ping"); + second.sendMessage("ping"); + yield first.awaitMessage("pong-alpha"); + yield second.awaitMessage("pong-beta"); + + const menu = yield openContextMenu(); + for (const id of ["alpha", "beta", "gamma"]) { + const expected = items.includes(id); + const found = menu.getElementsByAttribute("label", id); + is(found.length, expected, `menu item ${id} ${expected ? "" : "not "}found`); + } + // Return the first menu item, we need to click it. + return menu.getElementsByAttribute("label", items[0])[0]; + } + + // Confirm alpha, beta exist; click alpha to remove it. + const alpha = yield confirmMenuItems("alpha", "beta"); + yield closeExtensionContextMenu(alpha); + + // Confirm only beta exists. + yield confirmMenuItems("beta"); + yield closeContextMenu(); + + // Create gamma, confirm, click. + first.sendMessage("create"); + const beta = yield confirmMenuItems("beta", "gamma"); + yield closeExtensionContextMenu(beta); + + // Confirm only gamma is left. + yield confirmMenuItems("gamma"); + yield closeContextMenu(); + + yield first.unload(); + yield second.unload(); + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js new file mode 100644 index 000000000..a3fa9d32c --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js @@ -0,0 +1,96 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, + "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + + background: function() { + // Report onClickData info back. + browser.contextMenus.onClicked.addListener(info => { + browser.test.sendMessage("contextmenus-click", info); + }); + + browser.contextMenus.create({ + title: "Checkbox", + type: "checkbox", + }); + + browser.contextMenus.create({ + type: "separator", + }); + + browser.contextMenus.create({ + title: "Checkbox", + type: "checkbox", + checked: true, + }); + + browser.contextMenus.create({ + title: "Checkbox", + type: "checkbox", + }); + + browser.test.notifyPass("contextmenus-checkboxes"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("contextmenus-checkboxes"); + + function confirmCheckboxStates(extensionMenuRoot, expectedStates) { + let checkboxItems = extensionMenuRoot.getElementsByAttribute("type", "checkbox"); + + is(checkboxItems.length, 3, "there should be 3 checkbox items in the context menu"); + + is(checkboxItems[0].hasAttribute("checked"), expectedStates[0], `checkbox item 1 has state (checked=${expectedStates[0]})`); + is(checkboxItems[1].hasAttribute("checked"), expectedStates[1], `checkbox item 2 has state (checked=${expectedStates[1]})`); + is(checkboxItems[2].hasAttribute("checked"), expectedStates[2], `checkbox item 3 has state (checked=${expectedStates[2]})`); + + return extensionMenuRoot.getElementsByAttribute("type", "checkbox"); + } + + function confirmOnClickData(onClickData, id, was, checked) { + is(onClickData.wasChecked, was, `checkbox item ${id} was ${was ? "" : "not "}checked before the click`); + is(onClickData.checked, checked, `checkbox item ${id} is ${checked ? "" : "not "}checked after the click`); + } + + let extensionMenuRoot = yield openExtensionContextMenu(); + let items = confirmCheckboxStates(extensionMenuRoot, [false, true, false]); + yield closeExtensionContextMenu(items[0]); + + let result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 1, false, true); + + extensionMenuRoot = yield openExtensionContextMenu(); + items = confirmCheckboxStates(extensionMenuRoot, [true, true, false]); + yield closeExtensionContextMenu(items[2]); + + result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 3, false, true); + + extensionMenuRoot = yield openExtensionContextMenu(); + items = confirmCheckboxStates(extensionMenuRoot, [true, true, true]); + yield closeExtensionContextMenu(items[0]); + + result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 1, true, false); + + extensionMenuRoot = yield openExtensionContextMenu(); + items = confirmCheckboxStates(extensionMenuRoot, [false, true, true]); + yield closeExtensionContextMenu(items[2]); + + result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 3, true, false); + + yield extension.unload(); + yield BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js new file mode 100644 index 000000000..a3d31bd19 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js @@ -0,0 +1,76 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, + "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"); + + let encodedImageData = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC"; + let decodedImageData = atob(encodedImageData); + const IMAGE_ARRAYBUFFER = Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + "icons": { + "18": "extension.png", + }, + }, + + files: { + "extension.png": IMAGE_ARRAYBUFFER, + }, + + background: function() { + let menuitemId = browser.contextMenus.create({ + title: "child-to-delete", + onclick: () => { + browser.contextMenus.remove(menuitemId); + }, + }); + + browser.contextMenus.create({ + title: "child", + }); + + browser.test.onMessage.addListener(() => { + browser.test.sendMessage("pong"); + }); + browser.test.notifyPass("contextmenus-icons"); + }, + }); + + let confirmContextMenuIcon = (rootElement) => { + let expectedURL = new RegExp(String.raw`^moz-extension://[^/]+/extension\.png$`); + let imageUrl = rootElement.getAttribute("image"); + ok(expectedURL.test(imageUrl), "The context menu should display the extension icon next to the root element"); + }; + + yield extension.startup(); + yield extension.awaitFinish("contextmenus-icons"); + + let extensionMenu = yield openExtensionContextMenu(); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + let topLevelMenuItem = contextMenu.getElementsByAttribute("ext-type", "top-level-menu")[0]; + confirmContextMenuIcon(topLevelMenuItem); + + let childToDelete = extensionMenu.getElementsByAttribute("label", "child-to-delete")[0]; + yield closeExtensionContextMenu(childToDelete); + // Now perform a roundtrip to the extension process to make sure that the + // click event has had a chance to fire. + extension.sendMessage("ping"); + yield extension.awaitMessage("pong"); + + yield openExtensionContextMenu(); + + contextMenu = document.getElementById("contentAreaContextMenu"); + topLevelMenuItem = contextMenu.getElementsByAttribute("label", "child")[0]; + + confirmContextMenuIcon(topLevelMenuItem); + yield closeContextMenu(); + + yield extension.unload(); + yield BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js new file mode 100644 index 000000000..96453863d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js @@ -0,0 +1,196 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Loaded both as a background script and a tab page. +function testScript() { + let page = location.pathname.includes("tab.html") ? "tab" : "background"; + let clickCounts = { + old: 0, + new: 0, + }; + browser.contextMenus.onClicked.addListener(() => { + // Async to give other onclick handlers a chance to fire. + setTimeout(() => { + browser.test.sendMessage("onClicked-fired", page); + }); + }); + browser.test.onMessage.addListener((toPage, msg) => { + if (toPage !== page) { + return; + } + browser.test.log(`Received ${msg} for ${toPage}`); + if (msg == "get-click-counts") { + browser.test.sendMessage("click-counts", clickCounts); + } else if (msg == "clear-click-counts") { + clickCounts.old = clickCounts.new = 0; + browser.test.sendMessage("next"); + } else if (msg == "create-with-onclick") { + browser.contextMenus.create({ + id: "iden", + title: "tifier", + onclick() { + ++clickCounts.old; + browser.test.log(`onclick fired for original onclick property in ${page}`); + }, + }, () => browser.test.sendMessage("next")); + } else if (msg == "create-without-onclick") { + browser.contextMenus.create({ + id: "iden", + title: "tifier", + }, () => browser.test.sendMessage("next")); + } else if (msg == "update-without-onclick") { + browser.contextMenus.update("iden", { + enabled: true, // Already enabled, so this does nothing. + }, () => browser.test.sendMessage("next")); + } else if (msg == "update-with-onclick") { + browser.contextMenus.update("iden", { + onclick() { + ++clickCounts.new; + browser.test.log(`onclick fired for updated onclick property in ${page}`); + }, + }, () => browser.test.sendMessage("next")); + } else if (msg == "remove") { + browser.contextMenus.remove("iden", () => browser.test.sendMessage("next")); + } else if (msg == "removeAll") { + browser.contextMenus.removeAll(() => browser.test.sendMessage("next")); + } + }); + + if (page == "background") { + browser.test.log("Opening tab.html"); + browser.tabs.create({ + url: "tab.html", + active: false, // To not interfere with the context menu tests. + }); + } else { + // Sanity check - the pages must be in the same process. + let pages = browser.extension.getViews(); + browser.test.assertTrue(pages.includes(window), + "Expected this tab to be an extension view"); + pages = pages.filter(w => w !== window); + browser.test.assertEq(pages[0], browser.extension.getBackgroundPage(), + "Expected the other page to be a background page"); + browser.test.sendMessage("tab.html ready"); + } +} + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, + "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + background: testScript, + files: { + "tab.html": `<!DOCTYPE html><meta charset="utf-8"><script src="tab.js"></script>`, + "tab.js": testScript, + }, + }); + yield extension.startup(); + yield extension.awaitMessage("tab.html ready"); + + function* clickContextMenu() { + // Using openContextMenu instead of openExtensionContextMenu because the + // test extension has only one context menu item. + let extensionMenuRoot = yield openContextMenu(); + let items = extensionMenuRoot.getElementsByAttribute("label", "tifier"); + is(items.length, 1, "Expected one context menu item"); + yield closeExtensionContextMenu(items[0]); + // One of them is "tab", the other is "background". + info(`onClicked from: ${yield extension.awaitMessage("onClicked-fired")}`); + info(`onClicked from: ${yield extension.awaitMessage("onClicked-fired")}`); + } + + function* getCounts(page) { + extension.sendMessage(page, "get-click-counts"); + return yield extension.awaitMessage("click-counts"); + } + function* resetCounts() { + extension.sendMessage("tab", "clear-click-counts"); + extension.sendMessage("background", "clear-click-counts"); + yield extension.awaitMessage("next"); + yield extension.awaitMessage("next"); + } + + // During this test, at most one "onclick" attribute is expected at any time. + for (let pageOne of ["background", "tab"]) { + for (let pageTwo of ["background", "tab"]) { + info(`Testing with menu created by ${pageOne} and updated by ${pageTwo}`); + extension.sendMessage(pageOne, "create-with-onclick"); + yield extension.awaitMessage("next"); + + // Test that update without onclick attribute does not clear the existing + // onclick handler. + extension.sendMessage(pageTwo, "update-without-onclick"); + yield extension.awaitMessage("next"); + yield clickContextMenu(); + let clickCounts = yield getCounts(pageOne); + is(clickCounts.old, 1, `Original onclick should still be present in ${pageOne}`); + is(clickCounts.new, 0, `Not expecting any new handlers in ${pageOne}`); + if (pageOne !== pageTwo) { + clickCounts = yield getCounts(pageTwo); + is(clickCounts.old, 0, `Not expecting any handlers in ${pageTwo}`); + is(clickCounts.new, 0, `Not expecting any new handlers in ${pageTwo}`); + } + yield resetCounts(); + + // Test that update with onclick handler in a different page clears the + // existing handler and activates the new onclick handler. + extension.sendMessage(pageTwo, "update-with-onclick"); + yield extension.awaitMessage("next"); + yield clickContextMenu(); + clickCounts = yield getCounts(pageOne); + is(clickCounts.old, 0, `Original onclick should be gone from ${pageOne}`); + if (pageOne !== pageTwo) { + is(clickCounts.new, 0, `Still not expecting new handlers in ${pageOne}`); + } + clickCounts = yield getCounts(pageTwo); + if (pageOne !== pageTwo) { + is(clickCounts.old, 0, `Not expecting an old onclick in ${pageTwo}`); + } + is(clickCounts.new, 1, `New onclick should be triggered in ${pageTwo}`); + yield resetCounts(); + + // Test that updating the handler (different again from the last `update` + // call, but the same as the `create` call) clears the existing handler + // and activates the new onclick handler. + extension.sendMessage(pageOne, "update-with-onclick"); + yield extension.awaitMessage("next"); + yield clickContextMenu(); + clickCounts = yield getCounts(pageOne); + is(clickCounts.new, 1, `onclick should be triggered in ${pageOne}`); + if (pageOne !== pageTwo) { + clickCounts = yield getCounts(pageTwo); + is(clickCounts.new, 0, `onclick should be gone from ${pageTwo}`); + } + yield resetCounts(); + + // Test that removing the context menu and recreating it with the same ID + // (in a different context) does not leave behind any onclick handlers. + extension.sendMessage(pageTwo, "remove"); + yield extension.awaitMessage("next"); + extension.sendMessage(pageTwo, "create-without-onclick"); + yield extension.awaitMessage("next"); + yield clickContextMenu(); + clickCounts = yield getCounts(pageOne); + is(clickCounts.new, 0, `Did not expect any click handlers in ${pageOne}`); + if (pageOne !== pageTwo) { + clickCounts = yield getCounts(pageTwo); + is(clickCounts.new, 0, `Did not expect any click handlers in ${pageTwo}`); + } + yield resetCounts(); + + // Remove context menu for the next iteration of the test. And just to get + // more coverage, let's use removeAll instead of remove. + extension.sendMessage(pageOne, "removeAll"); + yield extension.awaitMessage("next"); + } + } + yield extension.unload(); + yield BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js new file mode 100644 index 000000000..3c5fa584b --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js @@ -0,0 +1,100 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, + "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + + background: function() { + // Report onClickData info back. + browser.contextMenus.onClicked.addListener(info => { + browser.test.sendMessage("contextmenus-click", info); + }); + + browser.contextMenus.create({ + title: "radio-group-1", + type: "radio", + checked: true, + }); + + browser.contextMenus.create({ + type: "separator", + }); + + browser.contextMenus.create({ + title: "radio-group-2", + type: "radio", + }); + + browser.contextMenus.create({ + title: "radio-group-2", + type: "radio", + }); + + browser.test.notifyPass("contextmenus-radio-groups"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("contextmenus-radio-groups"); + + function confirmRadioGroupStates(extensionMenuRoot, expectedStates) { + let radioItems = extensionMenuRoot.getElementsByAttribute("type", "radio"); + let radioGroup1 = extensionMenuRoot.getElementsByAttribute("label", "radio-group-1"); + let radioGroup2 = extensionMenuRoot.getElementsByAttribute("label", "radio-group-2"); + + is(radioItems.length, 3, "there should be 3 radio items in the context menu"); + is(radioGroup1.length, 1, "the first radio group should only have 1 radio item"); + is(radioGroup2.length, 2, "the second radio group should only have 2 radio items"); + + is(radioGroup1[0].hasAttribute("checked"), expectedStates[0], `radio item 1 has state (checked=${expectedStates[0]})`); + is(radioGroup2[0].hasAttribute("checked"), expectedStates[1], `radio item 2 has state (checked=${expectedStates[1]})`); + is(radioGroup2[1].hasAttribute("checked"), expectedStates[2], `radio item 3 has state (checked=${expectedStates[2]})`); + + return extensionMenuRoot.getElementsByAttribute("type", "radio"); + } + + function confirmOnClickData(onClickData, id, was, checked) { + is(onClickData.wasChecked, was, `radio item ${id} was ${was ? "" : "not "}checked before the click`); + is(onClickData.checked, checked, `radio item ${id} is ${checked ? "" : "not "}checked after the click`); + } + + let extensionMenuRoot = yield openExtensionContextMenu(); + let items = confirmRadioGroupStates(extensionMenuRoot, [true, false, false]); + yield closeExtensionContextMenu(items[1]); + + let result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 2, false, true); + + extensionMenuRoot = yield openExtensionContextMenu(); + items = confirmRadioGroupStates(extensionMenuRoot, [true, true, false]); + yield closeExtensionContextMenu(items[2]); + + result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 3, false, true); + + extensionMenuRoot = yield openExtensionContextMenu(); + items = confirmRadioGroupStates(extensionMenuRoot, [true, false, true]); + yield closeExtensionContextMenu(items[0]); + + result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 1, true, true); + + extensionMenuRoot = yield openExtensionContextMenu(); + items = confirmRadioGroupStates(extensionMenuRoot, [true, false, true]); + yield closeExtensionContextMenu(items[0]); + + result = yield extension.awaitMessage("contextmenus-click"); + confirmOnClickData(result, 1, true, true); + + yield extension.unload(); + yield BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js new file mode 100644 index 000000000..fdf06d656 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js @@ -0,0 +1,84 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, + "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"); + + // Install an extension. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + + background: function() { + browser.contextMenus.create({title: "a"}); + browser.contextMenus.create({title: "b"}); + browser.test.notifyPass("contextmenus-icons"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("contextmenus-icons"); + + // Open the context menu. + let contextMenu = yield openContextMenu("#img1"); + + // Confirm that the extension menu item exists. + let topLevelExtensionMenuItems = contextMenu.getElementsByAttribute("ext-type", "top-level-menu"); + is(topLevelExtensionMenuItems.length, 1, "the top level extension menu item exists"); + + yield closeContextMenu(); + + // Uninstall the extension. + yield extension.unload(); + + // Open the context menu. + contextMenu = yield openContextMenu("#img1"); + + // Confirm that the extension menu item has been removed. + topLevelExtensionMenuItems = contextMenu.getElementsByAttribute("ext-type", "top-level-menu"); + is(topLevelExtensionMenuItems.length, 0, "no top level extension menu items should exist"); + + yield closeContextMenu(); + + // Install a new extension. + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + background: function() { + browser.contextMenus.create({title: "c"}); + browser.contextMenus.create({title: "d"}); + browser.test.notifyPass("contextmenus-uninstall-second-extension"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("contextmenus-uninstall-second-extension"); + + // Open the context menu. + contextMenu = yield openContextMenu("#img1"); + + // Confirm that only the new extension menu item is in the context menu. + topLevelExtensionMenuItems = contextMenu.getElementsByAttribute("ext-type", "top-level-menu"); + is(topLevelExtensionMenuItems.length, 1, "only one top level extension menu item should exist"); + + // Close the context menu. + yield closeContextMenu(); + + // Uninstall the extension. + yield extension.unload(); + + // Open the context menu. + contextMenu = yield openContextMenu("#img1"); + + // Confirm that no extension menu items exist. + topLevelExtensionMenuItems = contextMenu.getElementsByAttribute("ext-type", "top-level-menu"); + is(topLevelExtensionMenuItems.length, 0, "no top level extension menu items should exist"); + + yield closeContextMenu(); + + yield BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js new file mode 100644 index 000000000..7849b8778 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js @@ -0,0 +1,254 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, + "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["contextMenus"], + }, + + background: function() { + // Test menu items using targetUrlPatterns. + browser.contextMenus.create({ + title: "targetUrlPatterns-patternMatches-contextAll", + targetUrlPatterns: ["*://*/*ctxmenu-image.png", "*://*/*some-link"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "targetUrlPatterns-patternMatches-contextImage", + targetUrlPatterns: ["*://*/*ctxmenu-image.png"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "targetUrlPatterns-patternMatches-contextLink", + targetUrlPatterns: ["*://*/*some-link"], + contexts: ["link"], + }); + + browser.contextMenus.create({ + title: "targetUrlPatterns-patternDoesNotMatch-contextAll", + targetUrlPatterns: ["*://*/does-not-match"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "targetUrlPatterns-patternDoesNotMatch-contextImage", + targetUrlPatterns: ["*://*/does-not-match"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "targetUrlPatterns-patternDoesNotMatch-contextLink", + targetUrlPatterns: ["*://*/does-not-match"], + contexts: ["link"], + }); + + // Test menu items using documentUrlPatterns. + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-contextAll", + documentUrlPatterns: ["*://*/*context.html"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-contextImage", + documentUrlPatterns: ["*://*/*context.html", "http://*/url-that-does-not-match"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-contextLink", + documentUrlPatterns: ["*://*/*context.html", "*://*/does-not-match"], + contexts: ["link"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-contextAll", + documentUrlPatterns: ["*://*/does-not-match"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-contextImage", + documentUrlPatterns: ["*://*/does-not-match"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-contextLink", + documentUrlPatterns: ["*://*/does-not-match"], + contexts: ["link"], + }); + + // Test menu items using both targetUrlPatterns and documentUrlPatterns. + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll", + documentUrlPatterns: ["*://*/*context.html"], + targetUrlPatterns: ["*://*/*ctxmenu-image.png"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll", + documentUrlPatterns: ["*://does-not-match"], + targetUrlPatterns: ["*://*/*ctxmenu-image.png"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll", + documentUrlPatterns: ["*://*/*context.html"], + targetUrlPatterns: ["*://does-not-match"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll", + documentUrlPatterns: ["*://does-not-match"], + targetUrlPatterns: ["*://does-not-match"], + contexts: ["all"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage", + documentUrlPatterns: ["*://*/*context.html"], + targetUrlPatterns: ["*://*/*ctxmenu-image.png"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage", + documentUrlPatterns: ["*://does-not-match"], + targetUrlPatterns: ["*://*/*ctxmenu-image.png"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage", + documentUrlPatterns: ["*://*/*context.html"], + targetUrlPatterns: ["*://does-not-match"], + contexts: ["image"], + }); + + browser.contextMenus.create({ + title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage", + documentUrlPatterns: ["*://does-not-match"], + targetUrlPatterns: ["*://does-not-match"], + contexts: ["image"], + }); + + browser.test.notifyPass("contextmenus-urlPatterns"); + }, + }); + + function* confirmContextMenuItems(menu, expected) { + for (let [label, shouldShow] of expected) { + let items = menu.getElementsByAttribute("label", label); + if (shouldShow) { + is(items.length, 1, `The menu item for label ${label} was correctly shown`); + } else { + is(items.length, 0, `The menu item for label ${label} was correctly not shown`); + } + } + } + + yield extension.startup(); + yield extension.awaitFinish("contextmenus-urlPatterns"); + + let extensionContextMenu = yield openExtensionContextMenu("#img1"); + let expected = [ + ["targetUrlPatterns-patternMatches-contextAll", true], + ["targetUrlPatterns-patternMatches-contextImage", true], + ["targetUrlPatterns-patternMatches-contextLink", false], + ["targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ["targetUrlPatterns-patternDoesNotMatch-contextLink", false], + ["documentUrlPatterns-patternMatches-contextAll", true], + ["documentUrlPatterns-patternMatches-contextImage", true], + ["documentUrlPatterns-patternMatches-contextLink", false], + ["documentUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-contextLink", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll", true], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage", true], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ]; + yield confirmContextMenuItems(extensionContextMenu, expected); + yield closeContextMenu(); + + let contextMenu = yield openContextMenu("body"); + expected = [ + ["targetUrlPatterns-patternMatches-contextAll", false], + ["targetUrlPatterns-patternMatches-contextImage", false], + ["targetUrlPatterns-patternMatches-contextLink", false], + ["targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ["targetUrlPatterns-patternDoesNotMatch-contextLink", false], + ["documentUrlPatterns-patternMatches-contextAll", true], + ["documentUrlPatterns-patternMatches-contextImage", false], + ["documentUrlPatterns-patternMatches-contextLink", false], + ["documentUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-contextLink", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage", false], + ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ]; + yield confirmContextMenuItems(contextMenu, expected); + yield closeContextMenu(); + + contextMenu = yield openContextMenu("#link1"); + expected = [ + ["targetUrlPatterns-patternMatches-contextAll", true], + ["targetUrlPatterns-patternMatches-contextImage", false], + ["targetUrlPatterns-patternMatches-contextLink", true], + ["targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ["targetUrlPatterns-patternDoesNotMatch-contextLink", false], + ["documentUrlPatterns-patternMatches-contextAll", true], + ["documentUrlPatterns-patternMatches-contextImage", false], + ["documentUrlPatterns-patternMatches-contextLink", true], + ["documentUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-contextLink", false], + ]; + yield confirmContextMenuItems(contextMenu, expected); + yield closeContextMenu(); + + contextMenu = yield openContextMenu("#img-wrapped-in-link"); + expected = [ + ["targetUrlPatterns-patternMatches-contextAll", true], + ["targetUrlPatterns-patternMatches-contextImage", true], + ["targetUrlPatterns-patternMatches-contextLink", true], + ["targetUrlPatterns-patternDoesNotMatch-contextAll", false], + ["targetUrlPatterns-patternDoesNotMatch-contextImage", false], + ["targetUrlPatterns-patternDoesNotMatch-contextLink", false], + ["documentUrlPatterns-patternMatches-contextAll", true], + ["documentUrlPatterns-patternMatches-contextImage", true], + ["documentUrlPatterns-patternMatches-contextLink", true], + ["documentUrlPatterns-patternDoesNotMatch-contextAll", false], + ["documentUrlPatterns-patternDoesNotMatch-contextImage", false], + ["documentUrlPatterns-patternDoesNotMatch-contextLink", false], + ]; + yield confirmContextMenuItems(contextMenu, expected); + yield closeContextMenu(); + + yield extension.unload(); + yield BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_currentWindow.js b/browser/components/extensions/test/browser/browser_ext_currentWindow.js new file mode 100644 index 000000000..11660bf4d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js @@ -0,0 +1,149 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function genericChecker() { + let kind = "background"; + let path = window.location.pathname; + if (path.includes("/popup.html")) { + kind = "popup"; + } else if (path.includes("/page.html")) { + kind = "page"; + } + + browser.test.onMessage.addListener((msg, ...args) => { + if (msg == kind + "-check-current1") { + browser.tabs.query({ + currentWindow: true, + }, function(tabs) { + browser.test.sendMessage("result", tabs[0].windowId); + }); + } else if (msg == kind + "-check-current2") { + browser.tabs.query({ + windowId: browser.windows.WINDOW_ID_CURRENT, + }, function(tabs) { + browser.test.sendMessage("result", tabs[0].windowId); + }); + } else if (msg == kind + "-check-current3") { + browser.windows.getCurrent(function(window) { + browser.test.sendMessage("result", window.id); + }); + } else if (msg == kind + "-open-page") { + browser.tabs.create({windowId: args[0], url: browser.runtime.getURL("page.html")}); + } else if (msg == kind + "-close-page") { + browser.tabs.query({ + windowId: args[0], + }, tabs => { + let tab = tabs.find(tab => tab.url.includes("/page.html")); + browser.tabs.remove(tab.id, () => { + browser.test.sendMessage("closed"); + }); + }); + } + }); + browser.test.sendMessage(kind + "-ready"); +} + +add_task(function* () { + let win1 = yield BrowserTestUtils.openNewBrowserWindow(); + let win2 = yield BrowserTestUtils.openNewBrowserWindow(); + + yield focusWindow(win2); + + yield BrowserTestUtils.loadURI(win1.gBrowser.selectedBrowser, "about:robots"); + yield BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser); + + yield BrowserTestUtils.loadURI(win2.gBrowser.selectedBrowser, "about:config"); + yield BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + + "browser_action": { + "default_popup": "popup.html", + }, + }, + + files: { + "page.html": ` + <!DOCTYPE html> + <html><body> + <script src="page.js"></script> + </body></html> + `, + + "page.js": genericChecker, + + "popup.html": ` + <!DOCTYPE html> + <html><body> + <script src="popup.js"></script> + </body></html> + `, + + "popup.js": genericChecker, + }, + + background: genericChecker, + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); + + let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let winId1 = WindowManager.getId(win1); + let winId2 = WindowManager.getId(win2); + + function* checkWindow(kind, winId, name) { + extension.sendMessage(kind + "-check-current1"); + is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 1) [${kind}]`); + extension.sendMessage(kind + "-check-current2"); + is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 2) [${kind}]`); + extension.sendMessage(kind + "-check-current3"); + is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 3) [${kind}]`); + } + + yield focusWindow(win1); + yield checkWindow("background", winId1, "win1"); + yield focusWindow(win2); + yield checkWindow("background", winId2, "win2"); + + function* triggerPopup(win, callback) { + yield clickBrowserAction(extension, win); + yield awaitExtensionPanel(extension, win); + + yield extension.awaitMessage("popup-ready"); + + yield callback(); + + closeBrowserAction(extension, win); + } + + // Set focus to some other window. + yield focusWindow(window); + + yield triggerPopup(win1, function* () { + yield checkWindow("popup", winId1, "win1"); + }); + + yield triggerPopup(win2, function* () { + yield checkWindow("popup", winId2, "win2"); + }); + + function* triggerPage(winId, name) { + extension.sendMessage("background-open-page", winId); + yield extension.awaitMessage("page-ready"); + yield checkWindow("page", winId, name); + extension.sendMessage("background-close-page", winId); + yield extension.awaitMessage("closed"); + } + + yield triggerPage(winId1, "win1"); + yield triggerPage(winId2, "win2"); + + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(win1); + yield BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_getViews.js b/browser/components/extensions/test/browser/browser_ext_getViews.js new file mode 100644 index 000000000..684e19ac5 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_getViews.js @@ -0,0 +1,198 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function genericChecker() { + let kind = "background"; + let path = window.location.pathname; + if (path.indexOf("popup") != -1) { + kind = "popup"; + } else if (path.indexOf("tab") != -1) { + kind = "tab"; + } + window.kind = kind; + + browser.test.onMessage.addListener((msg, ...args) => { + if (msg == kind + "-check-views") { + let windowId = args[0]; + let counts = { + "background": 0, + "tab": 0, + "popup": 0, + "kind": 0, + "window": 0, + }; + if (Number.isInteger(windowId)) { + counts.window = browser.extension.getViews({windowId: windowId}).length; + } + if (kind !== "background") { + counts.kind = browser.extension.getViews({type: kind}).length; + } + let views = browser.extension.getViews(); + let background; + for (let i = 0; i < views.length; i++) { + let view = views[i]; + browser.test.assertTrue(view.kind in counts, "view type is valid"); + counts[view.kind]++; + if (view.kind == "background") { + browser.test.assertTrue(view === browser.extension.getBackgroundPage(), + "background page is correct"); + background = view; + } + } + if (background) { + browser.runtime.getBackgroundPage().then(view => { + browser.test.assertEq(background, view, "runtime.getBackgroundPage() is correct"); + browser.test.sendMessage("counts", counts); + }); + } else { + browser.test.sendMessage("counts", counts); + } + } else if (msg == kind + "-open-tab") { + browser.tabs.create({windowId: args[0], url: browser.runtime.getURL("tab.html")}); + } else if (msg == kind + "-close-tab") { + browser.tabs.query({ + windowId: args[0], + }, tabs => { + let tab = tabs.find(tab => tab.url.indexOf("tab.html") != -1); + browser.tabs.remove(tab.id, () => { + browser.test.sendMessage("closed"); + }); + }); + } + }); + browser.test.sendMessage(kind + "-ready"); +} + +add_task(function* () { + let win1 = yield BrowserTestUtils.openNewBrowserWindow(); + let win2 = yield BrowserTestUtils.openNewBrowserWindow(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + + "browser_action": { + "default_popup": "popup.html", + }, + }, + + files: { + "tab.html": ` + <!DOCTYPE html> + <html><body> + <script src="tab.js"></script> + </body></html> + `, + + "tab.js": genericChecker, + + "popup.html": ` + <!DOCTYPE html> + <html><body> + <script src="popup.js"></script> + </body></html> + `, + + "popup.js": genericChecker, + }, + + background: genericChecker, + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); + + info("started"); + + let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let winId1 = WindowManager.getId(win1); + let winId2 = WindowManager.getId(win2); + + function* openTab(winId) { + extension.sendMessage("background-open-tab", winId); + yield extension.awaitMessage("tab-ready"); + } + + function* checkViews(kind, tabCount, popupCount, kindCount, windowId = undefined, windowCount = 0) { + extension.sendMessage(kind + "-check-views", windowId); + let counts = yield extension.awaitMessage("counts"); + is(counts.background, 1, "background count correct"); + is(counts.tab, tabCount, "tab count correct"); + is(counts.popup, popupCount, "popup count correct"); + is(counts.kind, kindCount, "count for type correct"); + is(counts.window, windowCount, "count for window correct"); + } + + yield checkViews("background", 0, 0, 0); + + yield openTab(winId1); + + yield checkViews("background", 1, 0, 0, winId1, 1); + yield checkViews("tab", 1, 0, 1); + + yield openTab(winId2); + + yield checkViews("background", 2, 0, 0, winId2, 1); + + function* triggerPopup(win, callback) { + yield clickBrowserAction(extension, win); + yield awaitExtensionPanel(extension, win); + + yield extension.awaitMessage("popup-ready"); + + yield callback(); + + closeBrowserAction(extension, win); + } + + // The popup occasionally closes prematurely if we open it immediately here. + // I'm not sure what causes it to close (it's something internal, and seems to + // be focus-related, but it's not caused by JS calling hidePopup), but even a + // short timeout seems to consistently fix it. + yield new Promise(resolve => win1.setTimeout(resolve, 10)); + + yield triggerPopup(win1, function* () { + yield checkViews("background", 2, 1, 0, winId1, 2); + yield checkViews("popup", 2, 1, 1); + }); + + yield triggerPopup(win2, function* () { + yield checkViews("background", 2, 1, 0, winId2, 2); + yield checkViews("popup", 2, 1, 1); + }); + + info("checking counts after popups"); + + yield checkViews("background", 2, 0, 0, winId1, 1); + + info("closing one tab"); + + extension.sendMessage("background-close-tab", winId1); + yield extension.awaitMessage("closed"); + + info("one tab closed, one remains"); + + yield checkViews("background", 1, 0, 0); + + info("opening win1 popup"); + + yield triggerPopup(win1, function* () { + yield checkViews("background", 1, 1, 0); + yield checkViews("tab", 1, 1, 1); + yield checkViews("popup", 1, 1, 1); + }); + + info("opening win2 popup"); + + yield triggerPopup(win2, function* () { + yield checkViews("background", 1, 1, 0); + yield checkViews("tab", 1, 1, 1); + yield checkViews("popup", 1, 1, 1); + }); + + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(win1); + yield BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_incognito_popup.js b/browser/components/extensions/test/browser/browser_ext_incognito_popup.js new file mode 100644 index 000000000..174b2179d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_incognito_popup.js @@ -0,0 +1,108 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testIncognitoPopup() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + "browser_action": { + "default_popup": "popup.html", + }, + "page_action": { + "default_popup": "popup.html", + }, + }, + + background: async function() { + let resolveMessage; + browser.runtime.onMessage.addListener(msg => { + if (resolveMessage && msg.message == "popup-details") { + resolveMessage(msg); + } + }); + + let awaitPopup = windowId => { + return new Promise(resolve => { + resolveMessage = resolve; + }).then(msg => { + browser.test.assertEq(windowId, msg.windowId, "Got popup message from correct window"); + return msg; + }); + }; + + let testWindow = async window => { + let [tab] = await browser.tabs.query({active: true, windowId: window.id}); + + await browser.pageAction.show(tab.id); + browser.test.sendMessage("click-pageAction"); + + let msg = await awaitPopup(window.id); + browser.test.assertEq(window.incognito, msg.incognito, "Correct incognito status in pageAction popup"); + + browser.test.sendMessage("click-browserAction"); + + msg = await awaitPopup(window.id); + browser.test.assertEq(window.incognito, msg.incognito, "Correct incognito status in browserAction popup"); + }; + + const URL = "http://example.com/incognito"; + let windowReady = new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId, changed, tab) { + if (changed.status == "complete" && tab.url == URL) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + + try { + { + let window = await browser.windows.getCurrent(); + + await testWindow(window); + } + + { + let window = await browser.windows.create({incognito: true, url: URL}); + await windowReady; + + await testWindow(window); + + await browser.windows.remove(window.id); + } + + browser.test.notifyPass("incognito"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("incognito"); + } + }, + + files: { + "popup.html": '<html><head><meta charset="utf-8"><script src="popup.js"></script></head></html>', + + "popup.js": async function() { + let win = await browser.windows.getCurrent(); + browser.runtime.sendMessage({ + message: "popup-details", + windowId: win.id, + incognito: browser.extension.inIncognitoContext, + }); + window.close(); + }, + }, + }); + + extension.onMessage("click-browserAction", () => { + clickBrowserAction(extension, Services.wm.getMostRecentWindow("navigator:browser")); + }); + + extension.onMessage("click-pageAction", () => { + clickPageAction(extension, Services.wm.getMostRecentWindow("navigator:browser")); + }); + + yield extension.startup(); + yield extension.awaitFinish("incognito"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_incognito_views.js b/browser/components/extensions/test/browser/browser_ext_incognito_views.js new file mode 100644 index 000000000..4865b2d4f --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_incognito_views.js @@ -0,0 +1,121 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testIncognitoViews() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + "browser_action": { + "default_popup": "popup.html", + }, + }, + + background: async function() { + window.isBackgroundPage = true; + + let resolveMessage; + browser.runtime.onMessage.addListener(msg => { + if (resolveMessage && msg.message == "popup-details") { + resolveMessage(msg); + } + }); + + let awaitPopup = windowId => { + return new Promise(resolve => { + resolveMessage = resolve; + }).then(msg => { + browser.test.assertEq(windowId, msg.windowId, "Got popup message from correct window"); + return msg; + }); + }; + + let testWindow = async window => { + browser.test.sendMessage("click-browserAction"); + + let msg = await awaitPopup(window.id); + browser.test.assertEq(window.incognito, msg.incognito, "Correct incognito status in browserAction popup"); + }; + + const URL = "http://example.com/incognito"; + let windowReady = new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId, changed, tab) { + if (changed.status == "complete" && tab.url == URL) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + + try { + { + let window = await browser.windows.getCurrent(); + + await testWindow(window); + } + + { + let window = await browser.windows.create({incognito: true, url: URL}); + await windowReady; + + await testWindow(window); + + await browser.windows.remove(window.id); + } + + browser.test.notifyPass("incognito-views"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("incognito-views"); + } + }, + + files: { + "popup.html": '<html><head><meta charset="utf-8"><script src="popup.js"></script></head></html>', + + "popup.js": async function() { + let views = browser.extension.getViews(); + + if (browser.extension.inIncognitoContext) { + let bgPage = browser.extension.getBackgroundPage(); + browser.test.assertEq(null, bgPage, "Should not be able to access background page in incognito context"); + + bgPage = await browser.runtime.getBackgroundPage(); + browser.test.assertEq(null, bgPage, "Should not be able to access background page in incognito context"); + + browser.test.assertEq(1, views.length, "Should only see one view in incognito popup"); + browser.test.assertEq(window, views[0], "This window should be the only view"); + } else { + let bgPage = browser.extension.getBackgroundPage(); + browser.test.assertEq(true, bgPage.isBackgroundPage, + "Should be able to access background page in non-incognito context"); + + bgPage = await browser.runtime.getBackgroundPage(); + browser.test.assertEq(true, bgPage.isBackgroundPage, + "Should be able to access background page in non-incognito context"); + + browser.test.assertEq(2, views.length, "Should only two views in non-incognito popup"); + browser.test.assertEq(bgPage, views[0], "The background page should be the first view"); + browser.test.assertEq(window, views[1], "This window should be the second view"); + } + + let win = await browser.windows.getCurrent(); + browser.runtime.sendMessage({ + message: "popup-details", + windowId: win.id, + incognito: browser.extension.inIncognitoContext, + }); + + window.close(); + }, + }, + }); + + extension.onMessage("click-browserAction", () => { + clickBrowserAction(extension, Services.wm.getMostRecentWindow("navigator:browser")); + }); + + yield extension.startup(); + yield extension.awaitFinish("incognito-views"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_lastError.js b/browser/components/extensions/test/browser/browser_ext_lastError.js new file mode 100644 index 000000000..499e709aa --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_lastError.js @@ -0,0 +1,55 @@ +"use strict"; + +function* sendMessage(options) { + function background(options) { + browser.runtime.sendMessage(result => { + browser.test.assertEq(undefined, result, "Argument value"); + if (options.checkLastError) { + let lastError = browser[options.checkLastError].lastError; + browser.test.assertEq("runtime.sendMessage's message argument is missing", + lastError && lastError.message, + "lastError value"); + } + browser.test.sendMessage("done"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${background})(${JSON.stringify(options)})`, + }); + + yield extension.startup(); + + yield extension.awaitMessage("done"); + + yield extension.unload(); +} + +add_task(function* testLastError() { + // Not necessary in browser-chrome tests, but monitorConsole gripes + // if we don't call it. + SimpleTest.waitForExplicitFinish(); + + // Check that we have no unexpected console messages when lastError is + // checked. + for (let api of ["extension", "runtime"]) { + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{message: /message argument is missing/, forbid: true}]); + }); + + yield sendMessage({checkLastError: api}); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; + } + + // Check that we do have a console message when lastError is not checked. + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{message: /Unchecked lastError value: Error: runtime.sendMessage's message argument is missing/}]); + }); + + yield sendMessage({}); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; +}); diff --git a/browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js b/browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js new file mode 100644 index 000000000..01557a745 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js @@ -0,0 +1,173 @@ +"use strict"; + +const { + LegacyExtensionContext, +} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {}); + +function promiseAddonStartup(extension) { + const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + return new Promise((resolve) => { + let listener = (evt, extensionInstance) => { + Management.off("startup", listener); + resolve(extensionInstance); + }; + Management.on("startup", listener); + }); +} + +/** + * This test case ensures that the LegacyExtensionContext can receive a connection + * from a content script and that the received port contains the expected sender + * tab info. + */ +add_task(function* test_legacy_extension_context_contentscript_connection() { + function backgroundScript() { + // Extract the assigned uuid from the background page url and send it + // in a test message. + let uuid = window.location.hostname; + + browser.test.onMessage.addListener(async msg => { + if (msg == "open-test-tab") { + let tab = await browser.tabs.create({url: "http://example.com/"}); + browser.test.sendMessage("get-expected-sender-info", + {uuid, tab}); + } else if (msg == "close-current-tab") { + try { + let [tab] = await browser.tabs.query({active: true}); + await browser.tabs.remove(tab.id); + browser.test.sendMessage("current-tab-closed", true); + } catch (e) { + browser.test.sendMessage("current-tab-closed", false); + } + } + }); + + browser.test.sendMessage("ready"); + } + + function contentScript() { + browser.runtime.sendMessage("webextension -> legacy_extension message", (reply) => { + browser.test.assertEq("legacy_extension -> webextension reply", reply, + "Got the expected reply from the LegacyExtensionContext"); + browser.test.sendMessage("got-reply-message"); + }); + + let port = browser.runtime.connect(); + + port.onMessage.addListener(msg => { + browser.test.assertEq("legacy_extension -> webextension port message", msg, + "Got the expected message from the LegacyExtensionContext"); + port.postMessage("webextension -> legacy_extension port message"); + }); + } + + let extensionData = { + background: `new ${backgroundScript}`, + manifest: { + content_scripts: [ + { + matches: ["http://example.com/*"], + js: ["content-script.js"], + }, + ], + }, + files: { + "content-script.js": `new ${contentScript}`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let waitForExtensionReady = extension.awaitMessage("ready"); + + let waitForExtensionInstance = promiseAddonStartup(extension); + + extension.startup(); + + let extensionInstance = yield waitForExtensionInstance; + + // Connect to the target extension.id as an external context + // using the given custom sender info. + let legacyContext = new LegacyExtensionContext(extensionInstance); + + let waitConnectPort = new Promise(resolve => { + let {browser} = legacyContext.api; + browser.runtime.onConnect.addListener(port => { + resolve(port); + }); + }); + + let waitMessage = new Promise(resolve => { + let {browser} = legacyContext.api; + browser.runtime.onMessage.addListener((singleMsg, msgSender, sendReply) => { + sendReply("legacy_extension -> webextension reply"); + resolve({singleMsg, msgSender}); + }); + }); + + is(legacyContext.envType, "legacy_extension", + "LegacyExtensionContext instance has the expected type"); + + ok(legacyContext.api, "Got the API object"); + + yield waitForExtensionReady; + + extension.sendMessage("open-test-tab"); + + let {uuid, tab} = yield extension.awaitMessage("get-expected-sender-info"); + + let {singleMsg, msgSender} = yield waitMessage; + is(singleMsg, "webextension -> legacy_extension message", + "Got the expected message"); + ok(msgSender, "Got a message sender object"); + + is(msgSender.id, uuid, "The sender has the expected id property"); + is(msgSender.url, "http://example.com/", "The sender has the expected url property"); + ok(msgSender.tab, "The sender has a tab property"); + is(msgSender.tab.id, tab.id, "The port sender has the expected tab.id"); + + // Wait confirmation that the reply has been received. + yield extension.awaitMessage("got-reply-message"); + + let port = yield waitConnectPort; + + ok(port, "Got the Port API object"); + ok(port.sender, "The port has a sender property"); + + is(port.sender.id, uuid, "The port sender has an id property"); + is(port.sender.url, "http://example.com/", "The port sender has the expected url property"); + ok(port.sender.tab, "The port sender has a tab property"); + is(port.sender.tab.id, tab.id, "The port sender has the expected tab.id"); + + let waitPortMessage = new Promise(resolve => { + port.onMessage.addListener((msg) => { + resolve(msg); + }); + }); + + port.postMessage("legacy_extension -> webextension port message"); + + let msg = yield waitPortMessage; + + is(msg, "webextension -> legacy_extension port message", + "LegacyExtensionContext received the expected message from the webextension"); + + let waitForDisconnect = new Promise(resolve => { + port.onDisconnect.addListener(resolve); + }); + + let waitForTestDone = extension.awaitMessage("current-tab-closed"); + + extension.sendMessage("close-current-tab"); + + yield waitForDisconnect; + + info("Got the disconnect event on tab closed"); + + let success = yield waitForTestDone; + + ok(success, "Test completed successfully"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_omnibox.js b/browser/components/extensions/test/browser/browser_ext_omnibox.js new file mode 100644 index 000000000..98d3573c5 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js @@ -0,0 +1,286 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* setup() { + const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF); + }); +} + +add_task(function* () { + let keyword = "test"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "omnibox": { + "keyword": keyword, + }, + }, + + background: function() { + browser.omnibox.onInputStarted.addListener(() => { + browser.test.sendMessage("on-input-started-fired"); + }); + + let synchronous = true; + let suggestions = null; + let suggestCallback = null; + + browser.omnibox.onInputChanged.addListener((text, suggest) => { + if (synchronous && suggestions) { + suggest(suggestions); + } else { + suggestCallback = suggest; + } + browser.test.sendMessage("on-input-changed-fired", {text}); + }); + + browser.omnibox.onInputCancelled.addListener(() => { + browser.test.sendMessage("on-input-cancelled-fired"); + }); + + browser.omnibox.onInputEntered.addListener((text, disposition) => { + browser.test.sendMessage("on-input-entered-fired", {text, disposition}); + }); + + browser.test.onMessage.addListener((msg, data) => { + switch (msg) { + case "set-suggestions": + suggestions = data.suggestions; + browser.test.sendMessage("suggestions-set"); + break; + case "set-default-suggestion": + browser.omnibox.setDefaultSuggestion(data.suggestion); + browser.test.sendMessage("default-suggestion-set"); + break; + case "set-synchronous": + synchronous = data.synchronous; + break; + case "test-multiple-suggest-calls": + suggestions.forEach(suggestion => suggestCallback([suggestion])); + browser.test.sendMessage("test-ready"); + break; + case "test-suggestions-after-delay": + Promise.resolve().then(() => { + suggestCallback(suggestions); + browser.test.sendMessage("test-ready"); + }); + break; + } + }); + }, + }); + + function* expectEvent(event, expected = {}) { + let actual = yield extension.awaitMessage(event); + if (expected.text) { + is(actual.text, expected.text, + `Expected "${event}" to have fired with text: "${expected.text}".`); + } + if (expected.disposition) { + is(actual.disposition, expected.disposition, + `Expected "${event}" to have fired with disposition: "${expected.disposition}".`); + } + } + + function* startInputSession() { + gURLBar.focus(); + gURLBar.value = keyword; + EventUtils.synthesizeKey(" ", {}); + yield expectEvent("on-input-started-fired"); + EventUtils.synthesizeKey("t", {}); + yield expectEvent("on-input-changed-fired", {text: "t"}); + return "t"; + } + + function* testInputEvents() { + gURLBar.focus(); + + // Start an input session by typing in <keyword><space>. + for (let letter of keyword) { + EventUtils.synthesizeKey(letter, {}); + } + EventUtils.synthesizeKey(" ", {}); + yield expectEvent("on-input-started-fired"); + + // We should expect input changed events now that the keyword is active. + EventUtils.synthesizeKey("b", {}); + yield expectEvent("on-input-changed-fired", {text: "b"}); + + EventUtils.synthesizeKey("c", {}); + yield expectEvent("on-input-changed-fired", {text: "bc"}); + + EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + yield expectEvent("on-input-changed-fired", {text: "b"}); + + // Even though the input is <keyword><space> We should not expect an + // input started event to fire since the keyword is active. + EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + yield expectEvent("on-input-changed-fired", {text: ""}); + + // Make the keyword inactive by hitting backspace. + EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + yield expectEvent("on-input-cancelled-fired"); + + // Activate the keyword by typing a space. + // Expect onInputStarted to fire. + EventUtils.synthesizeKey(" ", {}); + yield expectEvent("on-input-started-fired"); + + // onInputChanged should fire even if a space is entered. + EventUtils.synthesizeKey(" ", {}); + yield expectEvent("on-input-changed-fired", {text: " "}); + + // The active session should cancel if the input blurs. + gURLBar.blur(); + yield expectEvent("on-input-cancelled-fired"); + } + + function* testHeuristicResult(expectedText, setDefaultSuggestion) { + if (setDefaultSuggestion) { + extension.sendMessage("set-default-suggestion", { + suggestion: { + description: expectedText, + }, + }); + yield extension.awaitMessage("default-suggestion-set"); + } + + let text = yield startInputSession(); + + let item = gURLBar.popup.richlistbox.children[0]; + + is(item.getAttribute("title"), expectedText, + `Expected heuristic result to have title: "${expectedText}".`); + + is(item.getAttribute("displayurl"), `${keyword} ${text}`, + `Expected heuristic result to have displayurl: "${keyword} ${text}".`); + + EventUtils.synthesizeMouseAtCenter(item, {}); + + yield expectEvent("on-input-entered-fired", { + text, + disposition: "currentTab", + }); + } + + function* testDisposition(suggestionIndex, expectedDisposition, expectedText) { + yield startInputSession(); + + // Select the suggestion. + for (let i = 0; i < suggestionIndex; i++) { + EventUtils.synthesizeKey("VK_DOWN", {}); + } + + let item = gURLBar.popup.richlistbox.children[suggestionIndex]; + if (expectedDisposition == "currentTab") { + EventUtils.synthesizeMouseAtCenter(item, {}); + } else if (expectedDisposition == "newForegroundTab") { + EventUtils.synthesizeMouseAtCenter(item, {accelKey: true}); + } else if (expectedDisposition == "newBackgroundTab") { + EventUtils.synthesizeMouseAtCenter(item, {shiftKey: true, accelKey: true}); + } + + yield expectEvent("on-input-entered-fired", { + text: expectedText, + disposition: expectedDisposition, + }); + } + + function* testSuggestions(info) { + extension.sendMessage("set-synchronous", {synchronous: false}); + + function expectSuggestion({content, description}, index) { + let item = gURLBar.popup.richlistbox.children[index + 1]; // Skip the heuristic result. + + ok(!!item, "Expected item to exist"); + is(item.getAttribute("title"), description, + `Expected suggestion to have title: "${description}".`); + + is(item.getAttribute("displayurl"), `${keyword} ${content}`, + `Expected suggestion to have displayurl: "${keyword} ${content}".`); + } + + let text = yield startInputSession(); + + extension.sendMessage(info.test); + yield extension.awaitMessage("test-ready"); + + info.suggestions.forEach(expectSuggestion); + + EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {}); + yield expectEvent("on-input-entered-fired", { + text, + disposition: "currentTab", + }); + } + + yield setup(); + yield extension.startup(); + + yield testInputEvents(); + + // Test the heuristic result with default suggestions. + yield testHeuristicResult("Generated extension", false /* setDefaultSuggestion */); + yield testHeuristicResult("hello world", true /* setDefaultSuggestion */); + yield testHeuristicResult("foo bar", true /* setDefaultSuggestion */); + + let suggestions = [ + {content: "a", description: "select a"}, + {content: "b", description: "select b"}, + {content: "c", description: "select c"}, + ]; + + extension.sendMessage("set-suggestions", {suggestions}); + yield extension.awaitMessage("suggestions-set"); + + // Test each suggestion and search disposition. + yield testDisposition(1, "currentTab", suggestions[0].content); + yield testDisposition(2, "newForegroundTab", suggestions[1].content); + yield testDisposition(3, "newBackgroundTab", suggestions[2].content); + + extension.sendMessage("set-suggestions", {suggestions}); + yield extension.awaitMessage("suggestions-set"); + + // Test adding suggestions asynchronously. + yield testSuggestions({ + test: "test-multiple-suggest-calls", + skipHeuristic: true, + suggestions, + }); + yield testSuggestions({ + test: "test-suggestions-after-delay", + skipHeuristic: true, + suggestions, + }); + + // Start monitoring the console. + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: new RegExp(`The keyword provided is already registered: "${keyword}"`), + }]); + }); + + // Try registering another extension with the same keyword + let extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + "omnibox": { + "keyword": keyword, + }, + }, + }); + + yield extension2.startup(); + + // Stop monitoring the console and confirm the correct errors are logged. + SimpleTest.endMonitorConsole(); + yield waitForConsole; + + yield extension2.unload(); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js b/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js new file mode 100644 index 000000000..3e7342dd1 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js @@ -0,0 +1,66 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_tab_options_privileges() { + function backgroundScript() { + browser.runtime.onMessage.addListener(({msgName, tabId}) => { + if (msgName == "removeTabId") { + browser.tabs.remove(tabId).then(() => { + browser.test.notifyPass("options-ui-privileges"); + }).catch(error => { + browser.test.log(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("options-ui-privileges"); + }); + } + }); + browser.runtime.openOptionsPage(); + } + + async function optionsScript() { + try { + let [tab] = await browser.tabs.query({url: "http://example.com/"}); + browser.test.assertEq("http://example.com/", tab.url, "Got the expect tab"); + + tab = await browser.tabs.getCurrent(); + browser.runtime.sendMessage({msgName: "removeTabId", tabId: tab.id}); + } catch (error) { + browser.test.log(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("options-ui-privileges"); + } + } + + const ID = "options_privileges@tests.mozilla.org"; + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + + manifest: { + applications: {gecko: {id: ID}}, + "permissions": ["tabs"], + "options_ui": { + "page": "options.html", + }, + }, + files: { + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="options.js" type="text/javascript"></script> + </head> + </html>`, + "options.js": optionsScript, + }, + background: backgroundScript, + }); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + yield extension.startup(); + + yield extension.awaitFinish("options-ui-privileges"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js new file mode 100644 index 000000000..2c2a4cd2f --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js @@ -0,0 +1,178 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* global runTests */ + +Services.scriptloader.loadSubScript(new URL("head_pageAction.js", gTestPath).href, + this); + +add_task(function* testTabSwitchContext() { + yield runTests({ + manifest: { + "name": "Foo Extension", + + "page_action": { + "default_icon": "default.png", + "default_popup": "__MSG_popup__", + "default_title": "Default __MSG_title__ \u263a", + }, + + "default_locale": "en", + + "permissions": ["tabs"], + }, + + "files": { + "_locales/en/messages.json": { + "popup": { + "message": "default.html", + "description": "Popup", + }, + + "title": { + "message": "Title", + "description": "Title", + }, + }, + + "_locales/es_ES/messages.json": { + "popup": { + "message": "default.html", + "description": "Popup", + }, + + "title": { + "message": "T\u00edtulo", + "description": "Title", + }, + }, + + "default.png": imageBuffer, + "1.png": imageBuffer, + "2.png": imageBuffer, + }, + + getTests(tabs) { + let details = [ + {"icon": browser.runtime.getURL("default.png"), + "popup": browser.runtime.getURL("default.html"), + "title": "Default T\u00edtulo \u263a"}, + {"icon": browser.runtime.getURL("1.png"), + "popup": browser.runtime.getURL("default.html"), + "title": "Default T\u00edtulo \u263a"}, + {"icon": browser.runtime.getURL("2.png"), + "popup": browser.runtime.getURL("2.html"), + "title": "Title 2"}, + {"icon": browser.runtime.getURL("2.png"), + "popup": browser.runtime.getURL("2.html"), + "title": "Default T\u00edtulo \u263a"}, + ]; + + let promiseTabLoad = details => { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId, changed) { + if (tabId == details.id && changed.url == details.url) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + }; + + return [ + expect => { + browser.test.log("Initial state. No icon visible."); + expect(null); + }, + async expect => { + browser.test.log("Show the icon on the first tab, expect default properties."); + await browser.pageAction.show(tabs[0]); + expect(details[0]); + }, + expect => { + browser.test.log("Change the icon. Expect default properties excluding the icon."); + browser.pageAction.setIcon({tabId: tabs[0], path: "1.png"}); + expect(details[1]); + }, + async expect => { + browser.test.log("Create a new tab. No icon visible."); + let tab = await browser.tabs.create({active: true, url: "about:blank?0"}); + tabs.push(tab.id); + expect(null); + }, + expect => { + browser.test.log("Await tab load. No icon visible."); + expect(null); + }, + async expect => { + browser.test.log("Change properties. Expect new properties."); + let tabId = tabs[1]; + await browser.pageAction.show(tabId); + + browser.pageAction.setIcon({tabId, path: "2.png"}); + browser.pageAction.setPopup({tabId, popup: "2.html"}); + browser.pageAction.setTitle({tabId, title: "Title 2"}); + + expect(details[2]); + }, + async expect => { + browser.test.log("Change the hash. Expect same properties."); + + let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"}); + browser.tabs.update(tabs[1], {url: "about:blank?0#ref"}); + await promise; + + expect(details[2]); + }, + expect => { + browser.test.log("Clear the title. Expect default title."); + browser.pageAction.setTitle({tabId: tabs[1], title: ""}); + + expect(details[3]); + }, + async expect => { + browser.test.log("Navigate to a new page. Expect icon hidden."); + + // TODO: This listener should not be necessary, but the |tabs.update| + // callback currently fires too early in e10s windows. + let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"}); + + browser.tabs.update(tabs[1], {url: "about:blank?1"}); + + await promise; + expect(null); + }, + async expect => { + browser.test.log("Show the icon. Expect default properties again."); + + await browser.pageAction.show(tabs[1]); + expect(details[0]); + }, + async expect => { + browser.test.log("Switch back to the first tab. Expect previously set properties."); + await browser.tabs.update(tabs[0], {active: true}); + expect(details[1]); + }, + async expect => { + browser.test.log("Hide the icon on tab 2. Switch back, expect hidden."); + await browser.pageAction.hide(tabs[1]); + + await browser.tabs.update(tabs[1], {active: true}); + expect(null); + }, + async expect => { + browser.test.log("Switch back to tab 1. Expect previous results again."); + await browser.tabs.remove(tabs[1]); + expect(details[1]); + }, + async expect => { + browser.test.log("Hide the icon. Expect hidden."); + + await browser.pageAction.hide(tabs[0]); + expect(null); + }, + ]; + }, + }); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js new file mode 100644 index 000000000..83defdd68 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js @@ -0,0 +1,238 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testPageActionPopup() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head></html>`; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "background": { + "page": "data/background.html", + }, + "page_action": { + "default_popup": "popup-a.html", + }, + }, + + files: { + "popup-a.html": scriptPage("popup-a.js"), + "popup-a.js": function() { + window.onload = () => { + let background = window.getComputedStyle(document.body).backgroundColor; + browser.test.assertEq("transparent", background); + browser.runtime.sendMessage("from-popup-a"); + }; + browser.runtime.onMessage.addListener(msg => { + if (msg == "close-popup") { + window.close(); + } + }); + }, + + "data/popup-b.html": scriptPage("popup-b.js"), + "data/popup-b.js": function() { + browser.runtime.sendMessage("from-popup-b"); + }, + + "data/background.html": scriptPage("background.js"), + + "data/background.js": async function() { + let tabId; + + let sendClick; + let tests = [ + () => { + sendClick({expectEvent: false, expectPopup: "a"}); + }, + () => { + sendClick({expectEvent: false, expectPopup: "a"}); + }, + () => { + browser.pageAction.setPopup({tabId, popup: "popup-b.html"}); + sendClick({expectEvent: false, expectPopup: "b"}); + }, + () => { + sendClick({expectEvent: false, expectPopup: "b"}); + }, + () => { + browser.pageAction.setPopup({tabId, popup: ""}); + sendClick({expectEvent: true, expectPopup: null}); + }, + () => { + sendClick({expectEvent: true, expectPopup: null}); + }, + () => { + browser.pageAction.setPopup({tabId, popup: "/popup-a.html"}); + sendClick({expectEvent: false, expectPopup: "a", runNextTest: true}); + }, + () => { + browser.test.sendMessage("next-test", {expectClosed: true}); + }, + () => { + sendClick({expectEvent: false, expectPopup: "a", runNextTest: true}); + }, + () => { + browser.test.sendMessage("next-test", {closeOnTabSwitch: true}); + }, + ]; + + let expect = {}; + sendClick = ({expectEvent, expectPopup, runNextTest}) => { + expect = {event: expectEvent, popup: expectPopup, runNextTest}; + browser.test.sendMessage("send-click"); + }; + + browser.runtime.onMessage.addListener(msg => { + if (msg == "close-popup") { + return; + } else if (expect.popup) { + browser.test.assertEq(msg, `from-popup-${expect.popup}`, + "expected popup opened"); + } else { + browser.test.fail(`unexpected popup: ${msg}`); + } + + expect.popup = null; + if (expect.runNextTest) { + expect.runNextTest = false; + tests.shift()(); + } else { + browser.test.sendMessage("next-test"); + } + }); + + browser.pageAction.onClicked.addListener(() => { + if (expect.event) { + browser.test.succeed("expected click event received"); + } else { + browser.test.fail("unexpected click event"); + } + + expect.event = false; + browser.test.sendMessage("next-test"); + }); + + browser.test.onMessage.addListener(msg => { + if (msg == "close-popup") { + browser.runtime.sendMessage("close-popup"); + return; + } + + if (msg != "next-test") { + browser.test.fail("Expecting 'next-test' message"); + } + + if (tests.length) { + let test = tests.shift(); + test(); + } else { + browser.test.notifyPass("pageaction-tests-done"); + } + }); + + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + tabId = tab.id; + + await browser.pageAction.show(tabId); + browser.test.sendMessage("next-test"); + }, + }, + }); + + extension.onMessage("send-click", () => { + clickPageAction(extension); + }); + + let pageActionId, panelId; + extension.onMessage("next-test", Task.async(function* (expecting = {}) { + pageActionId = `${makeWidgetId(extension.id)}-page-action`; + panelId = `${makeWidgetId(extension.id)}-panel`; + let panel = document.getElementById(panelId); + if (expecting.expectClosed) { + ok(panel, "Expect panel to exist"); + yield promisePopupShown(panel); + + extension.sendMessage("close-popup"); + + yield promisePopupHidden(panel); + ok(true, `Panel is closed`); + } else if (expecting.closeOnTabSwitch) { + ok(panel, "Expect panel to exist"); + yield promisePopupShown(panel); + + let oldTab = gBrowser.selectedTab; + ok(oldTab != gBrowser.tabs[0], "Should have an inactive tab to switch to"); + + let hiddenPromise = promisePopupHidden(panel); + + gBrowser.selectedTab = gBrowser.tabs[0]; + yield hiddenPromise; + info("Panel closed"); + + gBrowser.selectedTab = oldTab; + } else if (panel) { + yield promisePopupShown(panel); + panel.hidePopup(); + } + + if (panel) { + panel = document.getElementById(panelId); + is(panel, null, "panel successfully removed from document after hiding"); + } + + extension.sendMessage("next-test"); + })); + + + yield extension.startup(); + yield extension.awaitFinish("pageaction-tests-done"); + + yield extension.unload(); + + let node = document.getElementById(pageActionId); + is(node, null, "pageAction image removed from document"); + + let panel = document.getElementById(panelId); + is(panel, null, "pageAction panel removed from document"); + + yield BrowserTestUtils.removeTab(tab); +}); + + +add_task(function* testPageActionSecurity() { + const URL = "chrome://browser/content/browser.xul"; + + let apis = ["browser_action", "page_action"]; + + for (let api of apis) { + info(`TEST ${api} icon url: ${URL}`); + + let messages = [/Access to restricted URI denied/]; + + let waitForConsole = new Promise(resolve => { + // Not necessary in browser-chrome tests, but monitorConsole gripes + // if we don't call it. + SimpleTest.waitForExplicitFinish(); + + SimpleTest.monitorConsole(resolve, messages); + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + [api]: {"default_popup": URL}, + }, + }); + + yield Assert.rejects(extension.startup(), + null, + "Manifest rejected"); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; + } +}); + +add_task(forceGC); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js new file mode 100644 index 000000000..98c4c3488 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js @@ -0,0 +1,169 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +let delay = ms => new Promise(resolve => { + setTimeout(resolve, ms); +}); + +add_task(function* testPageActionPopupResize() { + let browser; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "page_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + }, + background: function() { + /* global browser */ + browser.tabs.query({active: true, currentWindow: true}, tabs => { + const tabId = tabs[0].id; + + browser.pageAction.show(tabId).then(() => { + browser.test.sendMessage("action-shown"); + }); + }); + }, + + files: { + "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><div></div></body></html>`, + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("action-shown"); + + clickPageAction(extension, window); + + browser = yield awaitExtensionPanel(extension); + + function* checkSize(expected) { + let dims = yield promiseContentDimensions(browser); + let {body, root} = dims; + + is(dims.window.innerHeight, expected, `Panel window should be ${expected}px tall`); + is(body.clientHeight, body.scrollHeight, + "Panel body should be tall enough to fit its contents"); + is(root.clientHeight, root.scrollHeight, + "Panel root should be tall enough to fit its contents"); + + // Tolerate if it is 1px too wide, as that may happen with the current resizing method. + ok(Math.abs(dims.window.innerWidth - expected) <= 1, `Panel window should be ${expected}px wide`); + is(body.clientWidth, body.scrollWidth, + "Panel body should be wide enough to fit its contents"); + } + + /* eslint-disable mozilla/no-cpows-in-tests */ + function setSize(size) { + let elem = content.document.body.firstChild; + elem.style.height = `${size}px`; + elem.style.width = `${size}px`; + } + /* eslint-enable mozilla/no-cpows-in-tests */ + + let sizes = [ + 200, + 400, + 300, + ]; + + for (let size of sizes) { + yield alterContent(browser, setSize, size); + yield checkSize(size); + } + + yield alterContent(browser, setSize, 1400); + + let dims = yield promiseContentDimensions(browser); + let {body, root} = dims; + + if (AppConstants.platform == "win") { + while (dims.window.innerWidth < 800) { + yield delay(50); + dims = yield promiseContentDimensions(browser); + } + } + + is(dims.window.innerWidth, 800, "Panel window width"); + ok(body.clientWidth <= 800, `Panel body width ${body.clientWidth} is less than 800`); + is(body.scrollWidth, 1400, "Panel body scroll width"); + + is(dims.window.innerHeight, 600, "Panel window height"); + ok(root.clientHeight <= 600, `Panel root height (${root.clientHeight}px) is less than 600px`); + is(root.scrollHeight, 1400, "Panel root scroll height"); + + yield extension.unload(); +}); + +add_task(function* testPageActionPopupReflow() { + let browser; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "page_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + }, + background: function() { + browser.tabs.query({active: true, currentWindow: true}, tabs => { + const tabId = tabs[0].id; + + browser.pageAction.show(tabId).then(() => { + browser.test.sendMessage("action-shown"); + }); + }); + }, + + files: { + "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head> + <body> + The quick mauve fox jumps over the opalescent toad, with its glowing + eyes, and its vantablack mouth, and its bottomless chasm where you + would hope to find a heart, that looks straight into the deepest + pits of hell. The fox shivers, and cowers, and tries to run, but + the toad is utterly without pity. It turns, ever so slightly... + </body> + </html>`, + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("action-shown"); + + clickPageAction(extension, window); + + browser = yield awaitExtensionPanel(extension); + + /* eslint-disable mozilla/no-cpows-in-tests */ + function setSize(size) { + content.document.body.style.fontSize = `${size}px`; + } + /* eslint-enable mozilla/no-cpows-in-tests */ + + let dims = yield alterContent(browser, setSize, 18); + + if (AppConstants.platform == "win") { + while (dims.window.innerWidth < 800) { + yield delay(50); + dims = yield promiseContentDimensions(browser); + } + } + + is(dims.window.innerWidth, 800, "Panel window should be 800px wide"); + is(dims.body.clientWidth, 800, "Panel body should be 800px wide"); + is(dims.body.clientWidth, dims.body.scrollWidth, + "Panel body should be wide enough to fit its contents"); + + ok(dims.window.innerHeight > 36, + `Panel window height (${dims.window.innerHeight}px) should be taller than two lines of text.`); + + is(dims.body.clientHeight, dims.body.scrollHeight, + "Panel body should be tall enough to fit its contents"); + is(dims.root.clientHeight, dims.root.scrollHeight, + "Panel root should be tall enough to fit its contents"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js b/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js new file mode 100644 index 000000000..d1d173801 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js @@ -0,0 +1,60 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "page_action": { + "default_popup": "popup.html", + "unrecognized_property": "with-a-random-value", + }, + }, + + files: { + "popup.html": ` + <!DOCTYPE html> + <html><body> + <script src="popup.js"></script> + </body></html> + `, + + "popup.js": function() { + browser.runtime.sendMessage("from-popup"); + }, + }, + + background: function() { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(msg, "from-popup", "correct message received"); + browser.test.sendMessage("popup"); + }); + browser.tabs.query({active: true, currentWindow: true}, tabs => { + let tabId = tabs[0].id; + + browser.pageAction.show(tabId).then(() => { + browser.test.sendMessage("page-action-shown"); + }); + }); + }, + }); + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: /Reading manifest: Error processing page_action.unrecognized_property: An unexpected property was found/, + }]); + }); + + yield extension.startup(); + yield extension.awaitMessage("page-action-shown"); + + clickPageAction(extension); + + yield extension.awaitMessage("popup"); + + yield extension.unload(); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; +}); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_title.js b/browser/components/extensions/test/browser/browser_ext_pageAction_title.js new file mode 100644 index 000000000..793cd4499 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_title.js @@ -0,0 +1,226 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* global runTests */ + +Services.scriptloader.loadSubScript(new URL("head_pageAction.js", gTestPath).href, + this); + +add_task(function* testTabSwitchContext() { + yield runTests({ + manifest: { + "name": "Foo Extension", + + "page_action": { + "default_icon": "default.png", + "default_popup": "__MSG_popup__", + "default_title": "Default __MSG_title__ \u263a", + }, + + "default_locale": "en", + + "permissions": ["tabs"], + }, + + "files": { + "_locales/en/messages.json": { + "popup": { + "message": "default.html", + "description": "Popup", + }, + + "title": { + "message": "Title", + "description": "Title", + }, + }, + + "_locales/es_ES/messages.json": { + "popup": { + "message": "default.html", + "description": "Popup", + }, + + "title": { + "message": "T\u00edtulo", + "description": "Title", + }, + }, + + "default.png": imageBuffer, + "1.png": imageBuffer, + "2.png": imageBuffer, + }, + + getTests(tabs) { + let details = [ + {"icon": browser.runtime.getURL("default.png"), + "popup": browser.runtime.getURL("default.html"), + "title": "Default T\u00edtulo \u263a"}, + {"icon": browser.runtime.getURL("1.png"), + "popup": browser.runtime.getURL("default.html"), + "title": "Default T\u00edtulo \u263a"}, + {"icon": browser.runtime.getURL("2.png"), + "popup": browser.runtime.getURL("2.html"), + "title": "Title 2"}, + {"icon": browser.runtime.getURL("2.png"), + "popup": browser.runtime.getURL("2.html"), + "title": "Default T\u00edtulo \u263a"}, + ]; + + let promiseTabLoad = details => { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId, changed) { + if (tabId == details.id && changed.url == details.url) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + }; + return [ + expect => { + browser.test.log("Initial state. No icon visible."); + expect(null); + }, + async expect => { + browser.test.log("Show the icon on the first tab, expect default properties."); + await browser.pageAction.show(tabs[0]); + expect(details[0]); + }, + expect => { + browser.test.log("Change the icon. Expect default properties excluding the icon."); + browser.pageAction.setIcon({tabId: tabs[0], path: "1.png"}); + expect(details[1]); + }, + async expect => { + browser.test.log("Create a new tab. No icon visible."); + let tab = await browser.tabs.create({active: true, url: "about:blank?0"}); + tabs.push(tab.id); + expect(null); + }, + expect => { + browser.test.log("Await tab load. No icon visible."); + expect(null); + }, + async expect => { + browser.test.log("Change properties. Expect new properties."); + let tabId = tabs[1]; + + await browser.pageAction.show(tabId); + browser.pageAction.setIcon({tabId, path: "2.png"}); + browser.pageAction.setPopup({tabId, popup: "2.html"}); + browser.pageAction.setTitle({tabId, title: "Title 2"}); + + expect(details[2]); + }, + async expect => { + browser.test.log("Change the hash. Expect same properties."); + + let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"}); + + browser.tabs.update(tabs[1], {url: "about:blank?0#ref"}); + + await promise; + expect(details[2]); + }, + expect => { + browser.test.log("Clear the title. Expect default title."); + browser.pageAction.setTitle({tabId: tabs[1], title: ""}); + + expect(details[3]); + }, + async expect => { + browser.test.log("Navigate to a new page. Expect icon hidden."); + + // TODO: This listener should not be necessary, but the |tabs.update| + // callback currently fires too early in e10s windows. + let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"}); + + browser.tabs.update(tabs[1], {url: "about:blank?1"}); + + await promise; + expect(null); + }, + async expect => { + browser.test.log("Show the icon. Expect default properties again."); + await browser.pageAction.show(tabs[1]); + expect(details[0]); + }, + async expect => { + browser.test.log("Switch back to the first tab. Expect previously set properties."); + await browser.tabs.update(tabs[0], {active: true}); + expect(details[1]); + }, + async expect => { + browser.test.log("Hide the icon on tab 2. Switch back, expect hidden."); + await browser.pageAction.hide(tabs[1]); + await browser.tabs.update(tabs[1], {active: true}); + expect(null); + }, + async expect => { + browser.test.log("Switch back to tab 1. Expect previous results again."); + await browser.tabs.remove(tabs[1]); + expect(details[1]); + }, + async expect => { + browser.test.log("Hide the icon. Expect hidden."); + await browser.pageAction.hide(tabs[0]); + expect(null); + }, + ]; + }, + }); +}); + +add_task(function* testDefaultTitle() { + yield runTests({ + manifest: { + "name": "Foo Extension", + + "page_action": { + "default_icon": "icon.png", + }, + + "permissions": ["tabs"], + }, + + files: { + "icon.png": imageBuffer, + }, + + getTests(tabs) { + let details = [ + {"title": "Foo Extension", + "popup": "", + "icon": browser.runtime.getURL("icon.png")}, + {"title": "Foo Title", + "popup": "", + "icon": browser.runtime.getURL("icon.png")}, + ]; + + return [ + expect => { + browser.test.log("Initial state. No icon visible."); + expect(null); + }, + async expect => { + browser.test.log("Show the icon on the first tab, expect extension title as default title."); + await browser.pageAction.show(tabs[0]); + expect(details[0]); + }, + expect => { + browser.test.log("Change the title. Expect new title."); + browser.pageAction.setTitle({tabId: tabs[0], title: "Foo Title"}); + expect(details[1]); + }, + expect => { + browser.test.log("Clear the title. Expect extension title."); + browser.pageAction.setTitle({tabId: tabs[0], title: ""}); + expect(details[0]); + }, + ]; + }, + }); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js b/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js new file mode 100644 index 000000000..6f8a541a9 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js @@ -0,0 +1,101 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testPageActionPopup() { + const BASE = "http://example.com/browser/browser/components/extensions/test/browser"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": `${BASE}/file_popup_api_injection_a.html`, + }, + "page_action": { + "default_popup": `${BASE}/file_popup_api_injection_b.html`, + }, + }, + + files: { + "popup-a.html": `<html><head><meta charset="utf-8"> + <script type="application/javascript" src="popup-a.js"></script></head></html>`, + "popup-a.js": 'browser.test.sendMessage("from-popup-a");', + + "popup-b.html": `<html><head><meta charset="utf-8"> + <script type="application/javascript" src="popup-b.js"></script></head></html>`, + "popup-b.js": 'browser.test.sendMessage("from-popup-b");', + }, + + background: function() { + let tabId; + browser.tabs.query({active: true, currentWindow: true}, tabs => { + tabId = tabs[0].id; + browser.pageAction.show(tabId).then(() => { + browser.test.sendMessage("ready"); + }); + }); + + browser.test.onMessage.addListener(() => { + browser.browserAction.setPopup({popup: "/popup-a.html"}); + browser.pageAction.setPopup({tabId, popup: "popup-b.html"}); + + browser.test.sendMessage("ok"); + }); + }, + }); + + let promiseConsoleMessage = pattern => new Promise(resolve => { + Services.console.registerListener(function listener(msg) { + if (pattern.test(msg.message)) { + resolve(msg.message); + Services.console.unregisterListener(listener); + } + }); + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + + // Check that unprivileged documents don't get the API. + // BrowserAction: + let awaitMessage = promiseConsoleMessage(/WebExt Privilege Escalation: BrowserAction/); + SimpleTest.expectUncaughtException(); + yield clickBrowserAction(extension); + yield promisePopupShown(getBrowserActionPopup(extension)); + + let message = yield awaitMessage; + ok(message.includes("WebExt Privilege Escalation: BrowserAction: typeof(browser) = undefined"), + `No BrowserAction API injection`); + + yield closeBrowserAction(extension); + + // PageAction + awaitMessage = promiseConsoleMessage(/WebExt Privilege Escalation: PageAction/); + SimpleTest.expectUncaughtException(); + yield clickPageAction(extension); + + message = yield awaitMessage; + ok(message.includes("WebExt Privilege Escalation: PageAction: typeof(browser) = undefined"), + `No PageAction API injection: ${message}`); + + yield closePageAction(extension); + + SimpleTest.expectUncaughtException(false); + + + // Check that privileged documents *do* get the API. + extension.sendMessage("next"); + yield extension.awaitMessage("ok"); + + + yield clickBrowserAction(extension); + yield extension.awaitMessage("from-popup-a"); + yield promisePopupShown(getBrowserActionPopup(extension)); + yield closeBrowserAction(extension); + + yield clickPageAction(extension); + yield extension.awaitMessage("from-popup-b"); + yield closePageAction(extension); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_popup_background.js b/browser/components/extensions/test/browser/browser_ext_popup_background.js new file mode 100644 index 000000000..8b310c674 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_popup_background.js @@ -0,0 +1,133 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testPopupBackground() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.tabs.query({active: true, currentWindow: true}, tabs => { + browser.pageAction.show(tabs[0].id); + }); + }, + + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + + "page_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + }, + + files: { + "popup.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body style="width: 100px; height: 100px; background-color: green;"> + </body> + </html>`, + }, + }); + + yield extension.startup(); + + function* testPanel(browser, standAlone) { + let panel = getPanelForNode(browser); + let arrowContent = document.getAnonymousElementByAttribute(panel, "class", "panel-arrowcontent"); + let arrow = document.getAnonymousElementByAttribute(panel, "anonid", "arrow"); + + let borderColor = getComputedStyle(arrowContent).borderTopColor; + + let checkArrow = (background = null) => { + let image = getComputedStyle(arrow).listStyleImage; + + if (background == null || !standAlone) { + ok(image.startsWith('url("chrome://'), `We should have the built-in background image (got: ${image})`); + return; + } + + if (AppConstants.platform == "mac") { + // Panels have a drop shadow rather than a border on OS-X, so we extend + // the background color through the border area instead. + borderColor = background; + } + + image = decodeURIComponent(image); + let borderIndex = image.indexOf(`fill="${borderColor}"`); + let backgroundIndex = image.lastIndexOf(`fill="${background}"`); + + ok(borderIndex >= 0, `Have border fill (index=${borderIndex})`); + ok(backgroundIndex >= 0, `Have background fill (index=${backgroundIndex})`); + is(getComputedStyle(arrowContent).backgroundColor, background, "Arrow content should have correct background"); + isnot(borderIndex, backgroundIndex, "Border and background fills are separate elements"); + }; + + function getBackground(browser) { + return ContentTask.spawn(browser, null, function* () { + return content.getComputedStyle(content.document.body) + .backgroundColor; + }); + } + + /* eslint-disable mozilla/no-cpows-in-tests */ + let setBackground = color => { + content.document.body.style.backgroundColor = color; + }; + /* eslint-enable mozilla/no-cpows-in-tests */ + + yield new Promise(resolve => setTimeout(resolve, 100)); + + info("Test that initial background color is applied"); + + checkArrow(yield getBackground(browser)); + + info("Test that dynamically-changed background color is applied"); + + yield alterContent(browser, setBackground, "black"); + + checkArrow(yield getBackground(browser)); + + info("Test that non-opaque background color results in default styling"); + + yield alterContent(browser, setBackground, "rgba(1, 2, 3, .9)"); + + checkArrow(null); + } + + { + info("Test stand-alone browserAction popup"); + + clickBrowserAction(extension); + let browser = yield awaitExtensionPanel(extension); + yield testPanel(browser, true); + yield closeBrowserAction(extension); + } + + { + info("Test menu panel browserAction popup"); + + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL); + + clickBrowserAction(extension); + let browser = yield awaitExtensionPanel(extension); + yield testPanel(browser, false); + yield closeBrowserAction(extension); + } + + { + info("Test pageAction popup"); + + clickPageAction(extension); + let browser = yield awaitExtensionPanel(extension); + yield testPanel(browser, true); + yield closePageAction(extension); + } + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_popup_corners.js b/browser/components/extensions/test/browser/browser_ext_popup_corners.js new file mode 100644 index 000000000..52985ee46 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_popup_corners.js @@ -0,0 +1,98 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testPopupBorderRadius() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.tabs.query({active: true, currentWindow: true}, tabs => { + browser.pageAction.show(tabs[0].id); + }); + }, + + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + + "page_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + }, + + files: { + "popup.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body style="width: 100px; height: 100px;"></body> + </html>`, + }, + }); + + yield extension.startup(); + + function* testPanel(browser, standAlone = true) { + let panel = getPanelForNode(browser); + let arrowContent = document.getAnonymousElementByAttribute(panel, "class", "panel-arrowcontent"); + + let panelStyle = getComputedStyle(arrowContent); + + let viewNode = browser.parentNode === panel ? browser : browser.parentNode; + let viewStyle = getComputedStyle(viewNode); + + let props = ["borderTopLeftRadius", "borderTopRightRadius", + "borderBottomRightRadius", "borderBottomLeftRadius"]; + + /* eslint-disable mozilla/no-cpows-in-tests */ + let bodyStyle = yield ContentTask.spawn(browser, props, function* (props) { + let bodyStyle = content.getComputedStyle(content.document.body); + + return new Map(props.map(prop => [prop, bodyStyle[prop]])); + }); + /* eslint-enable mozilla/no-cpows-in-tests */ + + for (let prop of props) { + if (standAlone) { + is(viewStyle[prop], panelStyle[prop], `Panel and view ${prop} should be the same`); + is(bodyStyle.get(prop), panelStyle[prop], `Panel and body ${prop} should be the same`); + } else { + is(viewStyle[prop], "0px", `View node ${prop} should be 0px`); + is(bodyStyle.get(prop), "0px", `Body node ${prop} should be 0px`); + } + } + } + + { + info("Test stand-alone browserAction popup"); + + clickBrowserAction(extension); + let browser = yield awaitExtensionPanel(extension); + yield testPanel(browser); + yield closeBrowserAction(extension); + } + + { + info("Test menu panel browserAction popup"); + + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL); + + clickBrowserAction(extension); + let browser = yield awaitExtensionPanel(extension); + yield testPanel(browser, false); + yield closeBrowserAction(extension); + } + + { + info("Test pageAction popup"); + + clickPageAction(extension); + let browser = yield awaitExtensionPanel(extension); + yield testPanel(browser); + yield closePageAction(extension); + } + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js new file mode 100644 index 000000000..472ee7bbd --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js @@ -0,0 +1,93 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_popup_sendMessage_reply() { + let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head><body>${url}</body></html>`; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + + "page_action": { + "default_popup": "popup.html", + "browser_style": true, + }, + }, + + files: { + "popup.html": scriptPage("popup.js"), + "popup.js": async function() { + browser.runtime.onMessage.addListener(async msg => { + if (msg == "popup-ping") { + return Promise.resolve("popup-pong"); + } + }); + + let response = await browser.runtime.sendMessage("background-ping"); + browser.test.sendMessage("background-ping-response", response); + }, + }, + + async background() { + browser.runtime.onMessage.addListener(async msg => { + if (msg == "background-ping") { + let response = await browser.runtime.sendMessage("popup-ping"); + + browser.test.sendMessage("popup-ping-response", response); + + await new Promise(resolve => { + // Wait long enough that we're relatively sure the docShells have + // been swapped. Note that this value is fairly arbitrary. The load + // event that triggers the swap should happen almost immediately + // after the message is sent. The extra quarter of a second gives us + // enough leeway that we can expect to respond after the swap in the + // vast majority of cases. + setTimeout(resolve, 250); + }); + + return "background-pong"; + } + }); + + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + + await browser.pageAction.show(tab.id); + + browser.test.sendMessage("page-action-ready"); + }, + }); + + yield extension.startup(); + + { + clickBrowserAction(extension); + + let pong = yield extension.awaitMessage("background-ping-response"); + is(pong, "background-pong", "Got pong"); + + pong = yield extension.awaitMessage("popup-ping-response"); + is(pong, "popup-pong", "Got pong"); + + yield closeBrowserAction(extension); + } + + yield extension.awaitMessage("page-action-ready"); + + { + clickPageAction(extension); + + let pong = yield extension.awaitMessage("background-ping-response"); + is(pong, "background-pong", "Got pong"); + + pong = yield extension.awaitMessage("popup-ping-response"); + is(pong, "popup-pong", "Got pong"); + + yield closePageAction(extension); + } + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js new file mode 100644 index 000000000..66d37e857 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js @@ -0,0 +1,77 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +let getExtension = () => { + return ExtensionTestUtils.loadExtension({ + background: async function() { + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + await browser.pageAction.show(tab.id); + browser.test.sendMessage("pageAction ready"); + }, + + manifest: { + "browser_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + + "page_action": { + "default_popup": "popup.html", + "browser_style": false, + }, + }, + + files: { + "popup.html": `<!DOCTYPE html> + <html><head><meta charset="utf-8"></head></html>`, + }, + }); +}; + +add_task(function* testStandaloneBrowserAction() { + info("Test stand-alone browserAction popup"); + + let extension = getExtension(); + yield extension.startup(); + yield extension.awaitMessage("pageAction ready"); + + clickBrowserAction(extension); + let browser = yield awaitExtensionPanel(extension); + let panel = getPanelForNode(browser); + + yield extension.unload(); + + is(panel.parentNode, null, "Panel should be removed from the document"); +}); + +add_task(function* testMenuPanelBrowserAction() { + let extension = getExtension(); + yield extension.startup(); + yield extension.awaitMessage("pageAction ready"); + + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL); + + clickBrowserAction(extension); + let browser = yield awaitExtensionPanel(extension); + let panel = getPanelForNode(browser); + + yield extension.unload(); + + is(panel.state, "closed", "Panel should be closed"); +}); + +add_task(function* testPageAction() { + let extension = getExtension(); + yield extension.startup(); + yield extension.awaitMessage("pageAction ready"); + + clickPageAction(extension); + let browser = yield awaitExtensionPanel(extension); + let panel = getPanelForNode(browser); + + yield extension.unload(); + + is(panel.parentNode, null, "Panel should be removed from the document"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js new file mode 100644 index 000000000..1631ececa --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js @@ -0,0 +1,276 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +requestLongerTimeout(2); + +function add_tasks(task) { + add_task(task.bind(null, {embedded: false})); + + add_task(task.bind(null, {embedded: true})); +} + +function* loadExtension(options) { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + + embedded: options.embedded, + + manifest: Object.assign({ + "permissions": ["tabs"], + }, options.manifest), + + files: { + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="options.js" type="text/javascript"></script> + </head> + </html>`, + + "options.js": function() { + window.iAmOption = true; + browser.runtime.sendMessage("options.html"); + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "ping") { + respond("pong"); + } + }); + }, + }, + + background: options.background, + }); + + yield extension.startup(); + + return extension; +} + +add_tasks(function* test_inline_options(extraOptions) { + info(`Test options opened inline (${JSON.stringify(extraOptions)})`); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + let extension = yield loadExtension(Object.assign({}, extraOptions, { + manifest: { + applications: {gecko: {id: "inline_options@tests.mozilla.org"}}, + "options_ui": { + "page": "options.html", + }, + }, + + background: async function() { + let _optionsPromise; + let awaitOptions = () => { + browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already"); + + return new Promise(resolve => { + _optionsPromise = {resolve}; + }); + }; + + browser.runtime.onMessage.addListener((msg, sender) => { + if (msg == "options.html") { + if (_optionsPromise) { + _optionsPromise.resolve(sender.tab); + _optionsPromise = null; + } else { + browser.test.fail("Saw unexpected options page load"); + } + } + }); + + try { + let [firstTab] = await browser.tabs.query({currentWindow: true, active: true}); + + browser.test.log("Open options page. Expect fresh load."); + + let [, optionsTab] = await Promise.all([ + browser.runtime.openOptionsPage(), + awaitOptions(), + ]); + + browser.test.assertEq("about:addons", optionsTab.url, "Tab contains AddonManager"); + browser.test.assertTrue(optionsTab.active, "Tab is active"); + browser.test.assertTrue(optionsTab.id != firstTab.id, "Tab is a new tab"); + + browser.test.assertEq(0, browser.extension.getViews({type: "popup"}).length, "viewType is not popup"); + browser.test.assertEq(1, browser.extension.getViews({type: "tab"}).length, "viewType is tab"); + browser.test.assertEq(1, browser.extension.getViews({windowId: optionsTab.windowId}).length, "windowId matches"); + + let views = browser.extension.getViews(); + browser.test.assertEq(2, views.length, "Expected the options page and the background page"); + browser.test.assertTrue(views.includes(window), "One of the views is the background page"); + browser.test.assertTrue(views.some(w => w.iAmOption), "One of the views is the options page"); + + browser.test.log("Switch tabs."); + await browser.tabs.update(firstTab.id, {active: true}); + + browser.test.log("Open options page again. Expect tab re-selected, no new load."); + + await browser.runtime.openOptionsPage(); + let [tab] = await browser.tabs.query({currentWindow: true, active: true}); + + browser.test.assertEq(optionsTab.id, tab.id, "Tab is the same as the previous options tab"); + browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager"); + + browser.test.log("Ping options page."); + let pong = await browser.runtime.sendMessage("ping"); + browser.test.assertEq("pong", pong, "Got pong."); + + browser.test.log("Remove options tab."); + await browser.tabs.remove(optionsTab.id); + + browser.test.log("Open options page again. Expect fresh load."); + [, tab] = await Promise.all([ + browser.runtime.openOptionsPage(), + awaitOptions(), + ]); + browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager"); + browser.test.assertTrue(tab.active, "Tab is active"); + browser.test.assertTrue(tab.id != optionsTab.id, "Tab is a new tab"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("options-ui"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("options-ui"); + } + }, + })); + + yield extension.awaitFinish("options-ui"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_tasks(function* test_tab_options(extraOptions) { + info(`Test options opened in a tab (${JSON.stringify(extraOptions)})`); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + let extension = yield loadExtension(Object.assign({}, extraOptions, { + manifest: { + applications: {gecko: {id: "tab_options@tests.mozilla.org"}}, + "options_ui": { + "page": "options.html", + "open_in_tab": true, + }, + }, + + background: async function() { + let _optionsPromise; + let awaitOptions = () => { + browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already"); + + return new Promise(resolve => { + _optionsPromise = {resolve}; + }); + }; + + browser.runtime.onMessage.addListener((msg, sender) => { + if (msg == "options.html") { + if (_optionsPromise) { + _optionsPromise.resolve(sender.tab); + _optionsPromise = null; + } else { + browser.test.fail("Saw unexpected options page load"); + } + } + }); + + let optionsURL = browser.extension.getURL("options.html"); + + try { + let [firstTab] = await browser.tabs.query({currentWindow: true, active: true}); + + browser.test.log("Open options page. Expect fresh load."); + let [, optionsTab] = await Promise.all([ + browser.runtime.openOptionsPage(), + awaitOptions(), + ]); + browser.test.assertEq(optionsURL, optionsTab.url, "Tab contains options.html"); + browser.test.assertTrue(optionsTab.active, "Tab is active"); + browser.test.assertTrue(optionsTab.id != firstTab.id, "Tab is a new tab"); + + browser.test.assertEq(0, browser.extension.getViews({type: "popup"}).length, "viewType is not popup"); + browser.test.assertEq(1, browser.extension.getViews({type: "tab"}).length, "viewType is tab"); + browser.test.assertEq(1, browser.extension.getViews({windowId: optionsTab.windowId}).length, "windowId matches"); + + let views = browser.extension.getViews(); + browser.test.assertEq(2, views.length, "Expected the options page and the background page"); + browser.test.assertTrue(views.includes(window), "One of the views is the background page"); + browser.test.assertTrue(views.some(w => w.iAmOption), "One of the views is the options page"); + + browser.test.log("Switch tabs."); + await browser.tabs.update(firstTab.id, {active: true}); + + browser.test.log("Open options page again. Expect tab re-selected, no new load."); + + await browser.runtime.openOptionsPage(); + let [tab] = await browser.tabs.query({currentWindow: true, active: true}); + + browser.test.assertEq(optionsTab.id, tab.id, "Tab is the same as the previous options tab"); + browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html"); + + // Unfortunately, we can't currently do this, since onMessage doesn't + // currently support responses when there are multiple listeners. + // + // browser.test.log("Ping options page."); + // return new Promise(resolve => browser.runtime.sendMessage("ping", resolve)); + + browser.test.log("Remove options tab."); + await browser.tabs.remove(optionsTab.id); + + browser.test.log("Open options page again. Expect fresh load."); + [, tab] = await Promise.all([ + browser.runtime.openOptionsPage(), + awaitOptions(), + ]); + browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html"); + browser.test.assertTrue(tab.active, "Tab is active"); + browser.test.assertTrue(tab.id != optionsTab.id, "Tab is a new tab"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("options-ui-tab"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("options-ui-tab"); + } + }, + })); + + yield extension.awaitFinish("options-ui-tab"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_tasks(function* test_options_no_manifest(extraOptions) { + info(`Test with no manifest key (${JSON.stringify(extraOptions)})`); + + let extension = yield loadExtension(Object.assign({}, extraOptions, { + manifest: { + applications: {gecko: {id: "no_options@tests.mozilla.org"}}, + }, + + async background() { + browser.test.log("Try to open options page when not specified in the manifest."); + + await browser.test.assertRejects( + browser.runtime.openOptionsPage(), + /No `options_ui` declared/, + "Expected error from openOptionsPage()"); + + browser.test.notifyPass("options-no-manifest"); + }, + })); + + yield extension.awaitFinish("options-no-manifest"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js new file mode 100644 index 000000000..0c123b70e --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js @@ -0,0 +1,101 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* loadExtension(options) { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + + manifest: Object.assign({ + "permissions": ["tabs"], + }, options.manifest), + + files: { + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="options.js" type="text/javascript"></script> + </head> + </html>`, + + "options.js": function() { + browser.runtime.sendMessage("options.html"); + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "ping") { + respond("pong"); + } + }); + }, + }, + + background: options.background, + }); + + yield extension.startup(); + + return extension; +} + +add_task(function* test_inline_options_uninstall() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + let extension = yield loadExtension({ + manifest: { + applications: {gecko: {id: "inline_options_uninstall@tests.mozilla.org"}}, + "options_ui": { + "page": "options.html", + }, + }, + + background: async function() { + let _optionsPromise; + let awaitOptions = () => { + browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already"); + + return new Promise(resolve => { + _optionsPromise = {resolve}; + }); + }; + + browser.runtime.onMessage.addListener((msg, sender) => { + if (msg == "options.html") { + if (_optionsPromise) { + _optionsPromise.resolve(sender.tab); + _optionsPromise = null; + } else { + browser.test.fail("Saw unexpected options page load"); + } + } + }); + + try { + let [firstTab] = await browser.tabs.query({currentWindow: true, active: true}); + + browser.test.log("Open options page. Expect fresh load."); + let [, tab] = await Promise.all([ + browser.runtime.openOptionsPage(), + awaitOptions(), + ]); + + browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager"); + browser.test.assertTrue(tab.active, "Tab is active"); + browser.test.assertTrue(tab.id != firstTab.id, "Tab is a new tab"); + + browser.test.sendMessage("options-ui-open"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + } + }, + }); + + yield extension.awaitMessage("options-ui-open"); + yield extension.unload(); + + is(gBrowser.selectedBrowser.currentURI.spec, "about:addons", + "Add-on manager tab should still be open"); + + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js new file mode 100644 index 000000000..1c7ef4969 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js @@ -0,0 +1,94 @@ +"use strict"; + +let {AddonManager} = Components.utils.import("resource://gre/modules/AddonManager.jsm", {}); +let {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {}); + +function* makeAndInstallXPI(id, backgroundScript, loadedURL) { + let xpi = Extension.generateXPI({ + manifest: {applications: {gecko: {id}}}, + background: backgroundScript, + }); + SimpleTest.registerCleanupFunction(function cleanupXPI() { + Services.obs.notifyObservers(xpi, "flush-cache-entry", null); + xpi.remove(false); + }); + + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL); + + + info(`installing ${xpi.path}`); + let addon = yield AddonManager.installTemporaryAddon(xpi); + info("installed"); + + // A WebExtension is started asynchronously, we have our test extension + // open a new tab to signal that the background script has executed. + let loadTab = yield loadPromise; + yield BrowserTestUtils.removeTab(loadTab); + + return addon; +} + + +add_task(function* test_setuninstallurl_badargs() { + async function background() { + await browser.test.assertRejects( + browser.runtime.setUninstallURL("this is not a url"), + /Invalid URL/, + "setUninstallURL with an invalid URL should fail"); + + await browser.test.assertRejects( + browser.runtime.setUninstallURL("file:///etc/passwd"), + /must have the scheme http or https/, + "setUninstallURL with an illegal URL should fail"); + + browser.test.notifyPass("setUninstallURL bad params"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + }); + yield extension.startup(); + yield extension.awaitFinish(); + yield extension.unload(); +}); + +// Test the documented behavior of setUninstallURL() that passing an +// empty string is equivalent to not setting an uninstall URL +// (i.e., no new tab is opened upon uninstall) +add_task(function* test_setuninstall_empty_url() { + async function backgroundScript() { + await browser.runtime.setUninstallURL(""); + browser.tabs.create({url: "http://example.com/addon_loaded"}); + } + + let addon = yield makeAndInstallXPI("test_uinstallurl2@tests.mozilla.org", + backgroundScript, + "http://example.com/addon_loaded"); + + addon.uninstall(true); + info("uninstalled"); + + // no need to explicitly check for the absence of a new tab, + // BrowserTestUtils will eventually complain if one is opened. +}); + +add_task(function* test_setuninstallurl() { + async function backgroundScript() { + await browser.runtime.setUninstallURL("http://example.com/addon_uninstalled"); + browser.tabs.create({url: "http://example.com/addon_loaded"}); + } + + let addon = yield makeAndInstallXPI("test_uinstallurl@tests.mozilla.org", + backgroundScript, + "http://example.com/addon_loaded"); + + // look for a new tab with the uninstall url. + let uninstallPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/addon_uninstalled"); + + addon.uninstall(true); + info("uninstalled"); + + let uninstalledTab = yield uninstallPromise; + isnot(uninstalledTab, null, "opened tab with uninstall url"); + yield BrowserTestUtils.removeTab(uninstalledTab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js new file mode 100644 index 000000000..413f7bde6 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js @@ -0,0 +1,97 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals recordInitialTimestamps, onlyNewItemsFilter, checkRecentlyClosed */ + +requestLongerTimeout(2); + +Services.scriptloader.loadSubScript(new URL("head_sessions.js", gTestPath).href, + this); + +add_task(function* test_sessions_get_recently_closed() { + function* openAndCloseWindow(url = "http://example.com", tabUrls) { + let win = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, url); + yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + if (tabUrls) { + for (let url of tabUrls) { + yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); + } + } + yield BrowserTestUtils.closeWindow(win); + } + + function background() { + Promise.all([ + browser.sessions.getRecentlyClosed(), + browser.tabs.query({active: true, currentWindow: true}), + ]).then(([recentlyClosed, tabs]) => { + browser.test.sendMessage("initialData", {recentlyClosed, currentWindowId: tabs[0].windowId}); + }); + + browser.test.onMessage.addListener((msg, filter) => { + if (msg == "check-sessions") { + browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => { + browser.test.sendMessage("recentlyClosed", recentlyClosed); + }); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["sessions", "tabs"], + }, + background, + }); + + // Open and close a window that will be ignored, to prove that we are removing previous entries + yield openAndCloseWindow(); + + yield extension.startup(); + + let {recentlyClosed, currentWindowId} = yield extension.awaitMessage("initialData"); + recordInitialTimestamps(recentlyClosed.map(item => item.lastModified)); + + yield openAndCloseWindow(); + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 1, currentWindowId); + + yield openAndCloseWindow("about:config", ["about:robots", "about:mozilla"]); + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + // Check for multiple tabs in most recently closed window + is(recentlyClosed[0].window.tabs.length, 3, "most recently closed window has the expected number of tabs"); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com"); + yield BrowserTestUtils.removeTab(tab); + + tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com"); + yield BrowserTestUtils.removeTab(tab); + + yield openAndCloseWindow(); + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + let finalResult = recentlyClosed.filter(onlyNewItemsFilter); + checkRecentlyClosed(finalResult, 5, currentWindowId); + + isnot(finalResult[0].window, undefined, "first item is a window"); + is(finalResult[0].tab, undefined, "first item is not a tab"); + isnot(finalResult[1].tab, undefined, "second item is a tab"); + is(finalResult[1].window, undefined, "second item is not a window"); + isnot(finalResult[2].tab, undefined, "third item is a tab"); + is(finalResult[2].window, undefined, "third item is not a window"); + isnot(finalResult[3].window, undefined, "fourth item is a window"); + is(finalResult[3].tab, undefined, "fourth item is not a tab"); + isnot(finalResult[4].window, undefined, "fifth item is a window"); + is(finalResult[4].tab, undefined, "fifth item is not a tab"); + + // test with filter + extension.sendMessage("check-sessions", {maxResults: 2}); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 2, currentWindowId); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js new file mode 100644 index 000000000..217c8e130 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js @@ -0,0 +1,61 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals recordInitialTimestamps, onlyNewItemsFilter, checkRecentlyClosed */ + +SimpleTest.requestCompleteLog(); + +Services.scriptloader.loadSubScript(new URL("head_sessions.js", gTestPath).href, + this); + +add_task(function* test_sessions_get_recently_closed_private() { + function background() { + browser.test.onMessage.addListener((msg, filter) => { + if (msg == "check-sessions") { + browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => { + browser.test.sendMessage("recentlyClosed", recentlyClosed); + }); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["sessions", "tabs"], + }, + background, + }); + + // Open a private browsing window. + let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true}); + + yield extension.startup(); + + let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + let privateWinId = WindowManager.getId(privateWin); + + extension.sendMessage("check-sessions"); + let recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + recordInitialTimestamps(recentlyClosed.map(item => item.lastModified)); + + // Open and close two tabs in the private window + let tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com"); + yield BrowserTestUtils.removeTab(tab); + + tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com"); + yield BrowserTestUtils.removeTab(tab); + + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 2, privateWinId, true); + + // Close the private window. + yield BrowserTestUtils.closeWindow(privateWin); + + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + is(recentlyClosed.filter(onlyNewItemsFilter).length, 0, "the closed private window info was not found in recently closed data"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js new file mode 100644 index 000000000..ae0daff9a --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js @@ -0,0 +1,96 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function expectedTabInfo(tab, window) { + let browser = tab.linkedBrowser; + return { + url: browser.currentURI.spec, + title: browser.contentTitle, + favIconUrl: window.gBrowser.getIcon(tab), + }; +} + +function checkTabInfo(expected, actual) { + for (let prop in expected) { + is(actual[prop], expected[prop], `Expected value found for ${prop} of tab object.`); + } +} + +add_task(async function test_sessions_get_recently_closed_tabs() { + async function background() { + browser.test.onMessage.addListener(async msg => { + if (msg == "check-sessions") { + let recentlyClosed = await browser.sessions.getRecentlyClosed(); + browser.test.sendMessage("recentlyClosed", recentlyClosed); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["sessions", "tabs"], + }, + background, + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:addons"); + await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + let expectedTabs = []; + let tab = win.gBrowser.selectedTab; + expectedTabs.push(expectedTabInfo(tab, win)); + + for (let url of ["about:robots", "about:mozilla"]) { + tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); + expectedTabs.push(expectedTabInfo(tab, win)); + } + + await extension.startup(); + + // Test with a closed tab. + await BrowserTestUtils.removeTab(tab); + + extension.sendMessage("check-sessions"); + let recentlyClosed = await extension.awaitMessage("recentlyClosed"); + let tabInfo = recentlyClosed[0].tab; + let expectedTab = expectedTabs.pop(); + checkTabInfo(expectedTab, tabInfo); + + // Test with a closed window containing tabs. + await BrowserTestUtils.closeWindow(win); + + extension.sendMessage("check-sessions"); + recentlyClosed = await extension.awaitMessage("recentlyClosed"); + let tabInfos = recentlyClosed[0].window.tabs; + is(tabInfos.length, 2, "Expected number of tabs in closed window."); + for (let x = 0; x < tabInfos.length; x++) { + checkTabInfo(expectedTabs[x], tabInfos[x]); + } + + await extension.unload(); + + // Test without tabs permission. + extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["sessions"], + }, + background, + }); + + await extension.startup(); + + extension.sendMessage("check-sessions"); + recentlyClosed = await extension.awaitMessage("recentlyClosed"); + tabInfos = recentlyClosed[0].window.tabs; + is(tabInfos.length, 2, "Expected number of tabs in closed window."); + for (let tabInfo of tabInfos) { + for (let prop in expectedTabs[0]) { + is(undefined, + tabInfo[prop], + `${prop} of tab object is undefined without tabs permission.`); + } + } + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restore.js b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js new file mode 100644 index 000000000..6f1c6cf9a --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js @@ -0,0 +1,134 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +SimpleTest.requestCompleteLog(); + +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); + +add_task(function* test_sessions_restore() { + function background() { + browser.test.onMessage.addListener((msg, data) => { + if (msg == "check-sessions") { + browser.sessions.getRecentlyClosed().then(recentlyClosed => { + browser.test.sendMessage("recentlyClosed", recentlyClosed); + }); + } else if (msg == "restore") { + browser.sessions.restore(data).then(sessions => { + browser.test.sendMessage("restored", sessions); + }); + } else if (msg == "restore-reject") { + browser.sessions.restore("not-a-valid-session-id").then( + sessions => { + browser.test.fail("restore rejected with an invalid sessionId"); + }, + error => { + browser.test.assertTrue( + error.message.includes("Could not restore object using sessionId not-a-valid-session-id.")); + browser.test.sendMessage("restore-rejected"); + } + ); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["sessions", "tabs"], + }, + background, + }); + + yield extension.startup(); + + let {Management: {global: {WindowManager, TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + function checkLocalTab(tab, expectedUrl) { + let realTab = TabManager.getTab(tab.id); + let tabState = JSON.parse(SessionStore.getTabState(realTab)); + is(tabState.entries[0].url, expectedUrl, "restored tab has the expected url"); + } + + let win = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:config"); + yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + for (let url of ["about:robots", "about:mozilla"]) { + yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); + } + yield BrowserTestUtils.closeWindow(win); + + extension.sendMessage("check-sessions"); + let recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + + // Check that our expected window is the most recently closed. + is(recentlyClosed[0].window.tabs.length, 3, "most recently closed window has the expected number of tabs"); + + // Restore the window. + extension.sendMessage("restore"); + let restored = yield extension.awaitMessage("restored"); + + is(restored.length, 1, "restore returned the expected number of sessions"); + is(restored[0].window.tabs.length, 3, "restore returned a window with the expected number of tabs"); + checkLocalTab(restored[0].window.tabs[0], "about:config"); + checkLocalTab(restored[0].window.tabs[1], "about:robots"); + checkLocalTab(restored[0].window.tabs[2], "about:mozilla"); + + // Close the window again. + let window = WindowManager.getWindow(restored[0].window.id); + yield BrowserTestUtils.closeWindow(window); + + // Restore the window using the sessionId. + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + extension.sendMessage("restore", recentlyClosed[0].window.sessionId); + restored = yield extension.awaitMessage("restored"); + + is(restored.length, 1, "restore returned the expected number of sessions"); + is(restored[0].window.tabs.length, 3, "restore returned a window with the expected number of tabs"); + checkLocalTab(restored[0].window.tabs[0], "about:config"); + checkLocalTab(restored[0].window.tabs[1], "about:robots"); + checkLocalTab(restored[0].window.tabs[2], "about:mozilla"); + + // Close the window again. + window = WindowManager.getWindow(restored[0].window.id); + yield BrowserTestUtils.closeWindow(window); + + // Open and close a tab. + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); + yield BrowserTestUtils.removeTab(tab); + + // Restore the most recently closed item. + extension.sendMessage("restore"); + restored = yield extension.awaitMessage("restored"); + + is(restored.length, 1, "restore returned the expected number of sessions"); + tab = restored[0].tab; + ok(tab, "restore returned a tab"); + checkLocalTab(tab, "about:robots"); + + // Close the tab again. + let realTab = TabManager.getTab(tab.id); + yield BrowserTestUtils.removeTab(realTab); + + // Restore the tab using the sessionId. + extension.sendMessage("check-sessions"); + recentlyClosed = yield extension.awaitMessage("recentlyClosed"); + extension.sendMessage("restore", recentlyClosed[0].tab.sessionId); + restored = yield extension.awaitMessage("restored"); + + is(restored.length, 1, "restore returned the expected number of sessions"); + tab = restored[0].tab; + ok(tab, "restore returned a tab"); + checkLocalTab(tab, "about:robots"); + + // Close the tab again. + realTab = TabManager.getTab(tab.id); + yield BrowserTestUtils.removeTab(realTab); + + // Try to restore something with an invalid sessionId. + extension.sendMessage("restore-reject"); + restored = yield extension.awaitMessage("restore-rejected"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_simple.js b/browser/components/extensions/test/browser/browser_ext_simple.js new file mode 100644 index 000000000..ffa00c9db --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_simple.js @@ -0,0 +1,57 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_simple() { + let extensionData = { + manifest: { + "name": "Simple extension test", + "version": "1.0", + "manifest_version": 2, + "description": "", + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + info("load complete"); + yield extension.startup(); + info("startup complete"); + yield extension.unload(); + info("extension unloaded successfully"); +}); + +add_task(function* test_background() { + function backgroundScript() { + browser.test.log("running background script"); + + browser.test.onMessage.addListener((x, y) => { + browser.test.assertEq(x, 10, "x is 10"); + browser.test.assertEq(y, 20, "y is 20"); + + browser.test.notifyPass("background test passed"); + }); + + browser.test.sendMessage("running", 1); + } + + let extensionData = { + background: "(" + backgroundScript.toString() + ")()", + manifest: { + "name": "Simple extension test", + "version": "1.0", + "manifest_version": 2, + "description": "", + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + info("load complete"); + let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); + is(x, 1, "got correct value from extension"); + info("startup complete"); + extension.sendMessage(10, 20); + yield extension.awaitFinish(); + info("test complete"); + yield extension.unload(); + info("extension unloaded successfully"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js b/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js new file mode 100644 index 000000000..a5541a002 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js @@ -0,0 +1,74 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + let messages_received = []; + + let tabId; + + browser.runtime.onConnect.addListener((port) => { + browser.test.assertTrue(!!port, "tab to background port received"); + browser.test.assertEq("tab-connection-name", port.name, "port name should be defined and equal to connectInfo.name"); + browser.test.assertTrue(!!port.sender.tab, "port.sender.tab should be defined"); + browser.test.assertEq(tabId, port.sender.tab.id, "port.sender.tab.id should be equal to the expected tabId"); + + port.onMessage.addListener((msg) => { + messages_received.push(msg); + + if (messages_received.length == 1) { + browser.test.assertEq("tab to background port message", msg, "'tab to background' port message received"); + port.postMessage("background to tab port message"); + } + + if (messages_received.length == 2) { + browser.test.assertTrue(!!msg.tabReceived, "'background to tab' reply port message received"); + browser.test.assertEq("background to tab port message", msg.tabReceived, "reply port content contains the message received"); + + browser.test.notifyPass("tabRuntimeConnect.pass"); + } + }); + }); + + browser.tabs.create({url: "tab.html"}, + (tab) => { tabId = tab.id; }); + }, + + files: { + "tab.js": function() { + let port = browser.runtime.connect({name: "tab-connection-name"}); + port.postMessage("tab to background port message"); + port.onMessage.addListener((msg) => { + port.postMessage({tabReceived: msg}); + }); + }, + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <title>test tab extension page</title> + <meta charset="utf-8"> + <script src="tab.js" async></script> + </head> + <body> + <h1>test tab extension page</h1> + </body> + </html> + `, + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabRuntimeConnect.pass"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_audio.js b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js new file mode 100644 index 000000000..f9f6956d4 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js @@ -0,0 +1,203 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank?1"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank?2"); + + gBrowser.selectedTab = tab1; + + async function background() { + function promiseUpdated(tabId, attr) { + return new Promise(resolve => { + let onUpdated = (tabId_, changeInfo, tab) => { + if (tabId == tabId_ && attr in changeInfo) { + browser.tabs.onUpdated.removeListener(onUpdated); + + resolve({changeInfo, tab}); + } + }; + browser.tabs.onUpdated.addListener(onUpdated); + }); + } + + let deferred = {}; + browser.test.onMessage.addListener((message, tabId, result) => { + if (message == "change-tab-done" && deferred[tabId]) { + deferred[tabId].resolve(result); + } + }); + + function changeTab(tabId, attr, on) { + return new Promise((resolve, reject) => { + deferred[tabId] = {resolve, reject}; + browser.test.sendMessage("change-tab", tabId, attr, on); + }); + } + + + try { + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + browser.test.assertEq(tabs.length, 3, "We have three tabs"); + + for (let tab of tabs) { + // Note: We want to check that these are actual boolean values, not + // just that they evaluate as false. + browser.test.assertEq(false, tab.mutedInfo.muted, "Tab is not muted"); + browser.test.assertEq(undefined, tab.mutedInfo.reason, "Tab has no muted info reason"); + browser.test.assertEq(false, tab.audible, "Tab is not audible"); + } + + let windowId = tabs[0].windowId; + let tabIds = [tabs[1].id, tabs[2].id]; + + browser.test.log("Test initial queries for muted and audible return no tabs"); + let silent = await browser.tabs.query({windowId, audible: false}); + let audible = await browser.tabs.query({windowId, audible: true}); + let muted = await browser.tabs.query({windowId, muted: true}); + let nonMuted = await browser.tabs.query({windowId, muted: false}); + + browser.test.assertEq(3, silent.length, "Three silent tabs"); + browser.test.assertEq(0, audible.length, "No audible tabs"); + + browser.test.assertEq(0, muted.length, "No muted tabs"); + browser.test.assertEq(3, nonMuted.length, "Three non-muted tabs"); + + browser.test.log("Toggle muted and audible externally on one tab each, and check results"); + [muted, audible] = await Promise.all([ + promiseUpdated(tabIds[0], "mutedInfo"), + promiseUpdated(tabIds[1], "audible"), + changeTab(tabIds[0], "muted", true), + changeTab(tabIds[1], "audible", true), + ]); + + for (let obj of [muted.changeInfo, muted.tab]) { + browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted"); + browser.test.assertEq("user", obj.mutedInfo.reason, "Tab was muted by the user"); + } + + browser.test.assertEq(true, audible.changeInfo.audible, "Tab audible state changed"); + browser.test.assertEq(true, audible.tab.audible, "Tab is audible"); + + browser.test.log("Re-check queries. Expect one audible and one muted tab"); + silent = await browser.tabs.query({windowId, audible: false}); + audible = await browser.tabs.query({windowId, audible: true}); + muted = await browser.tabs.query({windowId, muted: true}); + nonMuted = await browser.tabs.query({windowId, muted: false}); + + browser.test.assertEq(2, silent.length, "Two silent tabs"); + browser.test.assertEq(1, audible.length, "One audible tab"); + + browser.test.assertEq(1, muted.length, "One muted tab"); + browser.test.assertEq(2, nonMuted.length, "Two non-muted tabs"); + + browser.test.assertEq(true, muted[0].mutedInfo.muted, "Tab is muted"); + browser.test.assertEq("user", muted[0].mutedInfo.reason, "Tab was muted by the user"); + + browser.test.assertEq(true, audible[0].audible, "Tab is audible"); + + browser.test.log("Toggle muted internally on two tabs, and check results"); + [nonMuted, muted] = await Promise.all([ + promiseUpdated(tabIds[0], "mutedInfo"), + promiseUpdated(tabIds[1], "mutedInfo"), + browser.tabs.update(tabIds[0], {muted: false}), + browser.tabs.update(tabIds[1], {muted: true}), + ]); + + for (let obj of [nonMuted.changeInfo, nonMuted.tab]) { + browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted"); + } + for (let obj of [muted.changeInfo, muted.tab]) { + browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted"); + } + + for (let obj of [nonMuted.changeInfo, nonMuted.tab, muted.changeInfo, muted.tab]) { + browser.test.assertEq("extension", obj.mutedInfo.reason, "Mute state changed by extension"); + + // FIXME: browser.runtime.id is currently broken. + browser.test.assertEq(browser.i18n.getMessage("@@extension_id"), + obj.mutedInfo.extensionId, + "Mute state changed by extension"); + } + + browser.test.log("Test that mutedInfo is preserved by sessionstore"); + let tab = await changeTab(tabIds[1], "duplicate").then(browser.tabs.get); + + browser.test.assertEq(true, tab.mutedInfo.muted, "Tab is muted"); + + browser.test.assertEq("extension", tab.mutedInfo.reason, "Mute state changed by extension"); + + // FIXME: browser.runtime.id is currently broken. + browser.test.assertEq(browser.i18n.getMessage("@@extension_id"), + tab.mutedInfo.extensionId, + "Mute state changed by extension"); + + browser.test.log("Unmute externally, and check results"); + [nonMuted] = await Promise.all([ + promiseUpdated(tabIds[1], "mutedInfo"), + changeTab(tabIds[1], "muted", false), + browser.tabs.remove(tab.id), + ]); + + for (let obj of [nonMuted.changeInfo, nonMuted.tab]) { + browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted"); + browser.test.assertEq("user", obj.mutedInfo.reason, "Mute state changed by user"); + } + + browser.test.notifyPass("tab-audio"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tab-audio"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + extension.onMessage("change-tab", (tabId, attr, on) => { + let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let tab = TabManager.getTab(tabId); + + if (attr == "muted") { + // Ideally we'd simulate a click on the tab audio icon for this, but the + // handler relies on CSS :hover states, which are complicated and fragile + // to simulate. + if (tab.muted != on) { + tab.toggleMuteAudio(); + } + } else if (attr == "audible") { + let browser = tab.linkedBrowser; + if (on) { + browser.audioPlaybackStarted(); + } else { + browser.audioPlaybackStopped(); + } + } else if (attr == "duplicate") { + // This is a bit of a hack. It won't be necessary once we have + // `tabs.duplicate`. + let newTab = gBrowser.duplicateTab(tab); + BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => { + extension.sendMessage("change-tab-done", tabId, TabManager.getId(newTab)); + }); + return; + } + + extension.sendMessage("change-tab-done", tabId); + }); + + yield extension.startup(); + + yield extension.awaitFinish("tab-audio"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js b/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js new file mode 100644 index 000000000..1491a19ab --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js @@ -0,0 +1,155 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* runTest(options) { + options.neutral = [0xaa, 0xaa, 0xaa]; + + let html = ` + <!DOCTYPE html> + <html lang="en"> + <head><meta charset="UTF-8"></head> + <body style="background-color: rgb(${options.color})"> + <!-- Fill most of the image with a neutral color to test edge-to-edge scaling. --> + <div style="position: absolute; + left: 2px; + right: 2px; + top: 2px; + bottom: 2px; + background: rgb(${options.neutral});"></div> + </body> + </html> + `; + + let url = `data:text/html,${encodeURIComponent(html)}`; + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, true); + + tab.linkedBrowser.fullZoom = options.fullZoom; + + async function background(options) { + browser.test.log(`Test color ${options.color} at fullZoom=${options.fullZoom}`); + + try { + let [tab] = await browser.tabs.query({currentWindow: true, active: true}); + + let [jpeg, png, ...pngs] = await Promise.all([ + browser.tabs.captureVisibleTab(tab.windowId, {format: "jpeg", quality: 95}), + browser.tabs.captureVisibleTab(tab.windowId, {format: "png", quality: 95}), + browser.tabs.captureVisibleTab(tab.windowId, {quality: 95}), + browser.tabs.captureVisibleTab(tab.windowId), + ]); + + browser.test.assertTrue(pngs.every(url => url == png), "All PNGs are identical"); + + browser.test.assertTrue(jpeg.startsWith("data:image/jpeg;base64,"), "jpeg is JPEG"); + browser.test.assertTrue(png.startsWith("data:image/png;base64,"), "png is PNG"); + + let promises = [jpeg, png].map(url => new Promise(resolve => { + let img = new Image(); + img.src = url; + img.onload = () => resolve(img); + })); + + [jpeg, png] = await Promise.all(promises); + let tabDims = `${tab.width}\u00d7${tab.height}`; + + let images = {jpeg, png}; + for (let format of Object.keys(images)) { + let img = images[format]; + + let dims = `${img.width}\u00d7${img.height}`; + browser.test.assertEq(tabDims, dims, `${format} dimensions are correct`); + + let canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + canvas.mozOpaque = true; + + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + + // Check the colors of the first and last pixels of the image, to make + // sure we capture the entire frame, and scale it correctly. + let coords = [ + {x: 0, y: 0, + color: options.color}, + {x: img.width - 1, + y: img.height - 1, + color: options.color}, + {x: img.width / 2 | 0, + y: img.height / 2 | 0, + color: options.neutral}, + ]; + + for (let {x, y, color} of coords) { + let imageData = ctx.getImageData(x, y, 1, 1).data; + + if (format == "png") { + browser.test.assertEq(`rgba(${color},255)`, `rgba(${[...imageData]})`, `${format} image color is correct at (${x}, ${y})`); + } else { + // Allow for some deviation in JPEG version due to lossy compression. + const SLOP = 3; + + browser.test.log(`Testing ${format} image color at (${x}, ${y}), have rgba(${[...imageData]}), expecting approx. rgba(${color},255)`); + + browser.test.assertTrue(Math.abs(color[0] - imageData[0]) <= SLOP, `${format} image color.red is correct at (${x}, ${y})`); + browser.test.assertTrue(Math.abs(color[1] - imageData[1]) <= SLOP, `${format} image color.green is correct at (${x}, ${y})`); + browser.test.assertTrue(Math.abs(color[2] - imageData[2]) <= SLOP, `${format} image color.blue is correct at (${x}, ${y})`); + browser.test.assertEq(255, imageData[3], `${format} image color.alpha is correct at (${x}, ${y})`); + } + } + } + + browser.test.notifyPass("captureVisibleTab"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("captureVisibleTab"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["<all_urls>"], + }, + + background: `(${background})(${JSON.stringify(options)})`, + }); + + yield extension.startup(); + + yield extension.awaitFinish("captureVisibleTab"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +} + +add_task(function* testCaptureVisibleTab() { + yield runTest({color: [0, 0, 0], fullZoom: 1}); + + yield runTest({color: [0, 0, 0], fullZoom: 2}); + + yield runTest({color: [0, 0, 0], fullZoom: 0.5}); + + yield runTest({color: [255, 255, 255], fullZoom: 1}); +}); + +add_task(function* testCaptureVisibleTabPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background() { + browser.test.assertFalse("captureVisibleTab" in browser.tabs, + 'Extension without "<all_tabs>" permission should not have access to captureVisibleTab'); + browser.test.notifyPass("captureVisibleTabPermissions"); + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("captureVisibleTabPermissions"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js new file mode 100644 index 000000000..dc0647e3c --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js @@ -0,0 +1,156 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* setup() { + // make sure userContext is enabled. + return SpecialPowers.pushPrefEnv({"set": [ + ["privacy.userContext.enabled", true], + ]}); +}); + +add_task(function* () { + info("Start testing tabs.create with cookieStoreId"); + + let testCases = [ + // No private window + {privateTab: false, cookieStoreId: null, success: true, expectedCookieStoreId: "firefox-default"}, + {privateTab: false, cookieStoreId: "firefox-default", success: true, expectedCookieStoreId: "firefox-default"}, + {privateTab: false, cookieStoreId: "firefox-container-1", success: true, expectedCookieStoreId: "firefox-container-1"}, + {privateTab: false, cookieStoreId: "firefox-container-2", success: true, expectedCookieStoreId: "firefox-container-2"}, + {privateTab: false, cookieStoreId: "firefox-container-42", failure: "exist"}, + {privateTab: false, cookieStoreId: "firefox-private", failure: "defaultToPrivate"}, + {privateTab: false, cookieStoreId: "wow", failure: "illegal"}, + + // Private window + {privateTab: true, cookieStoreId: null, success: true, expectedCookieStoreId: "firefox-private"}, + {privateTab: true, cookieStoreId: "firefox-private", success: true, expectedCookieStoreId: "firefox-private"}, + {privateTab: true, cookieStoreId: "firefox-default", failure: "privateToDefault"}, + {privateTab: true, cookieStoreId: "firefox-container-1", failure: "privateToDefault"}, + {privateTab: true, cookieStoreId: "wow", failure: "illegal"}, + ]; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs", "cookies"], + }, + + background: function() { + function testTab(data, tab) { + browser.test.assertTrue(data.success, "we want a success"); + browser.test.assertTrue(!!tab, "we have a tab"); + browser.test.assertEq(data.expectedCookieStoreId, tab.cookieStoreId, "tab should have the correct cookieStoreId"); + } + + async function runTest(data) { + try { + // Tab Creation + let tab; + try { + tab = await browser.tabs.create({ + windowId: data.privateTab ? this.privateWindowId : this.defaultWindowId, + cookieStoreId: data.cookieStoreId, + }); + + browser.test.assertTrue(!data.failure, "we want a success"); + } catch (error) { + browser.test.assertTrue(!!data.failure, "we want a failure"); + + if (data.failure == "illegal") { + browser.test.assertTrue(/Illegal cookieStoreId/.test(error.message), + "runtime.lastError should report the expected error message"); + } else if (data.failure == "defaultToPrivate") { + browser.test.assertTrue("Illegal to set private cookieStorageId in a non private window", + error.message, + "runtime.lastError should report the expected error message"); + } else if (data.failure == "privateToDefault") { + browser.test.assertTrue("Illegal to set non private cookieStorageId in a private window", + error.message, + "runtime.lastError should report the expected error message"); + } else if (data.failure == "exist") { + browser.test.assertTrue(/No cookie store exists/.test(error.message), + "runtime.lastError should report the expected error message"); + } else { + browser.test.fail("The test is broken"); + } + + browser.test.sendMessage("test-done"); + return; + } + + // Tests for tab creation + testTab(data, tab); + + { + // Tests for tab querying + let [tab] = await browser.tabs.query({ + windowId: data.privateTab ? this.privateWindowId : this.defaultWindowId, + cookieStoreId: data.cookieStoreId, + }); + + browser.test.assertTrue(tab != undefined, "Tab found!"); + testTab(data, tab); + } + + let stores = await browser.cookies.getAllCookieStores(); + + let store = stores.find(store => store.id === tab.cookieStoreId); + browser.test.assertTrue(!!store, "We have a store for this tab."); + + await browser.tabs.remove(tab.id); + + browser.test.sendMessage("test-done"); + } catch (e) { + browser.test.fail("An exception has been thrown"); + } + } + + async function initialize() { + let win = await browser.windows.create({incognito: true}); + this.privateWindowId = win.id; + + win = await browser.windows.create({incognito: false}); + this.defaultWindowId = win.id; + + browser.test.sendMessage("ready"); + } + + async function shutdown() { + await browser.windows.remove(this.privateWindowId); + await browser.windows.remove(this.defaultWindowId); + browser.test.sendMessage("gone"); + } + + // Waiting for messages + browser.test.onMessage.addListener((msg, data) => { + if (msg == "be-ready") { + initialize(); + } else if (msg == "test") { + runTest(data); + } else { + browser.test.assertTrue("finish", msg, "Shutting down"); + shutdown(); + } + }); + }, + }); + + yield extension.startup(); + + info("Tests must be ready..."); + extension.sendMessage("be-ready"); + yield extension.awaitMessage("ready"); + info("Tests are ready to run!"); + + for (let test of testCases) { + info(`test tab.create with cookieStoreId: "${test.cookieStoreId}"`); + extension.sendMessage("test", test); + yield extension.awaitMessage("test-done"); + } + + info("Waiting for shutting down..."); + extension.sendMessage("finish"); + yield extension.awaitMessage("gone"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create.js b/browser/components/extensions/test/browser/browser_ext_tabs_create.js new file mode 100644 index 000000000..8bc5a68a2 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_create.js @@ -0,0 +1,166 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); + gBrowser.selectedTab = tab; + + // TODO: Multiple windows. + + // Using pre-loaded new tab pages interferes with onUpdated events. + // It probably shouldn't. + SpecialPowers.setBoolPref("browser.newtab.preload", false); + registerCleanupFunction(() => { + SpecialPowers.clearUserPref("browser.newtab.preload"); + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + + "background": {"page": "bg/background.html"}, + }, + + files: { + "bg/blank.html": `<html><head><meta charset="utf-8"></head></html>`, + + "bg/background.html": `<html><head> + <meta charset="utf-8"> + <script src="background.js"></script> + </head></html>`, + + "bg/background.js": function() { + let activeTab; + let activeWindow; + + function runTests() { + const DEFAULTS = { + index: 2, + windowId: activeWindow, + active: true, + pinned: false, + url: "about:newtab", + }; + + let tests = [ + { + create: {url: "http://example.com/"}, + result: {url: "http://example.com/"}, + }, + { + create: {url: "blank.html"}, + result: {url: browser.runtime.getURL("bg/blank.html")}, + }, + { + create: {}, + result: {url: "about:newtab"}, + }, + { + create: {active: false}, + result: {active: false}, + }, + { + create: {active: true}, + result: {active: true}, + }, + { + create: {pinned: true}, + result: {pinned: true, index: 0}, + }, + { + create: {pinned: true, active: true}, + result: {pinned: true, active: true, index: 0}, + }, + { + create: {pinned: true, active: false}, + result: {pinned: true, active: false, index: 0}, + }, + { + create: {index: 1}, + result: {index: 1}, + }, + { + create: {index: 1, active: false}, + result: {index: 1, active: false}, + }, + { + create: {windowId: activeWindow}, + result: {windowId: activeWindow}, + }, + ]; + + async function nextTest() { + if (!tests.length) { + browser.test.notifyPass("tabs.create"); + return; + } + + let test = tests.shift(); + let expected = Object.assign({}, DEFAULTS, test.result); + + browser.test.log(`Testing tabs.create(${JSON.stringify(test.create)}), expecting ${JSON.stringify(test.result)}`); + + let updatedPromise = new Promise(resolve => { + let onUpdated = (changedTabId, changed) => { + if (changed.url) { + browser.tabs.onUpdated.removeListener(onUpdated); + resolve({tabId: changedTabId, url: changed.url}); + } + }; + browser.tabs.onUpdated.addListener(onUpdated); + }); + + let createdPromise = new Promise(resolve => { + let onCreated = tab => { + browser.test.assertTrue("id" in tab, `Expected tabs.onCreated callback to receive tab object`); + resolve(); + }; + browser.tabs.onCreated.addListener(onCreated); + }); + + let [tab] = await Promise.all([ + browser.tabs.create(test.create), + createdPromise, + ]); + let tabId = tab.id; + + for (let key of Object.keys(expected)) { + if (key === "url") { + // FIXME: This doesn't get updated until later in the load cycle. + continue; + } + + browser.test.assertEq(expected[key], tab[key], `Expected value for tab.${key}`); + } + + let updated = await updatedPromise; + browser.test.assertEq(tabId, updated.tabId, `Expected value for tab.id`); + browser.test.assertEq(expected.url, updated.url, `Expected value for tab.url`); + + await browser.tabs.remove(tabId); + await browser.tabs.update(activeTab, {active: true}); + + nextTest(); + } + + nextTest(); + } + + browser.tabs.query({active: true, currentWindow: true}, tabs => { + activeTab = tabs[0].id; + activeWindow = tabs[0].windowId; + + runTests(); + }); + }, + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.create"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); + diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js new file mode 100644 index 000000000..49938bf22 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js @@ -0,0 +1,66 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* testTabsCreateInvalidURL(tabsCreateURL) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.test.sendMessage("ready"); + browser.test.onMessage.addListener((msg, tabsCreateURL) => { + browser.tabs.create({url: tabsCreateURL}, (tab) => { + browser.test.assertEq(undefined, tab, "on error tab should be undefined"); + browser.test.assertTrue(/Illegal URL/.test(browser.runtime.lastError.message), + "runtime.lastError should report the expected error message"); + + // Remove the opened tab is any. + if (tab) { + browser.tabs.remove(tab.id); + } + browser.test.sendMessage("done"); + }); + }); + }, + }); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + info(`test tab.create on invalid URL "${tabsCreateURL}"`); + + extension.sendMessage("start", tabsCreateURL); + yield extension.awaitMessage("done"); + + yield extension.unload(); +} + +add_task(function* () { + info("Start testing tabs.create on invalid URLs"); + + let dataURLPage = `data:text/html, + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>data url page</h1> + </body> + </html>`; + + let testCases = [ + {tabsCreateURL: "about:addons"}, + {tabsCreateURL: "javascript:console.log('tabs.update execute javascript')"}, + {tabsCreateURL: dataURLPage}, + ]; + + for (let {tabsCreateURL} of testCases) { + yield* testTabsCreateInvalidURL(tabsCreateURL); + } + + info("done"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js b/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js new file mode 100644 index 000000000..f28606001 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js @@ -0,0 +1,47 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testDetectLanguage() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: async function() { + const BASE_PATH = "browser/browser/components/extensions/test/browser"; + + function loadTab(url) { + return browser.tabs.create({url}); + } + + try { + let tab = await loadTab(`http://example.co.jp/${BASE_PATH}/file_language_ja.html`); + let lang = await browser.tabs.detectLanguage(tab.id); + browser.test.assertEq("ja", lang, "Japanese document should be detected as Japanese"); + await browser.tabs.remove(tab.id); + + tab = await loadTab(`http://example.co.jp/${BASE_PATH}/file_language_fr_en.html`); + lang = await browser.tabs.detectLanguage(tab.id); + browser.test.assertEq("fr", lang, "French/English document should be detected as primarily French"); + await browser.tabs.remove(tab.id); + + tab = await loadTab(`http://example.co.jp/${BASE_PATH}/file_language_tlh.html`); + lang = await browser.tabs.detectLanguage(tab.id); + browser.test.assertEq("und", lang, "Klingon document should not be detected, should return 'und'"); + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("detectLanguage"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("detectLanguage"); + } + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("detectLanguage"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js new file mode 100644 index 000000000..c4b0ffd2d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js @@ -0,0 +1,146 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testDuplicateTab() { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + lastFocusedWindow: true, + }, function(tabs) { + let source = tabs[1]; + // By moving it 0, we check that the new tab is created next + // to the existing one. + browser.tabs.move(source.id, {index: 0}, () => { + browser.tabs.duplicate(source.id, (tab) => { + browser.test.assertEq("http://example.net/", tab.url); + // Should be the second tab, next to the one duplicated. + browser.test.assertEq(1, tab.index); + // Should be selected by default. + browser.test.assertTrue(tab.selected); + browser.test.notifyPass("tabs.duplicate"); + }); + }); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.duplicate"); + yield extension.unload(); + + while (gBrowser.tabs[0].linkedBrowser.currentURI.spec === "http://example.net/") { + yield BrowserTestUtils.removeTab(gBrowser.tabs[0]); + } +}); + +add_task(function* testDuplicateTabLazily() { + async function background() { + let tabLoadComplete = new Promise(resolve => { + browser.test.onMessage.addListener((message, tabId, result) => { + if (message == "duplicate-tab-done") { + resolve(tabId); + } + }); + }); + + function awaitLoad(tabId) { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) { + if (tabId == tabId_ && changed.status == "complete") { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + } + + try { + let url = "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html"; + let tab = await browser.tabs.create({url}); + let startTabId = tab.id; + + await awaitLoad(startTabId); + browser.test.sendMessage("duplicate-tab", startTabId); + + let unloadedTabId = await tabLoadComplete; + let loadedtab = await browser.tabs.get(startTabId); + browser.test.assertEq("Dummy test page", loadedtab.title, "Title should be returned for loaded pages"); + browser.test.assertEq("complete", loadedtab.status, "Tab status should be complete for loaded pages"); + + let unloadedtab = await browser.tabs.get(unloadedTabId); + browser.test.assertEq("Dummy test page", unloadedtab.title, "Title should be returned after page has been unloaded"); + + await browser.tabs.remove([tab.id, unloadedTabId]); + browser.test.notifyPass("tabs.hasCorrectTabTitle"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tabs.hasCorrectTabTitle"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + extension.onMessage("duplicate-tab", tabId => { + let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let tab = TabManager.getTab(tabId); + // This is a bit of a hack to load a tab in the background. + let newTab = gBrowser.duplicateTab(tab, false); + + BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => { + extension.sendMessage("duplicate-tab-done", TabManager.getId(newTab)); + }); + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.hasCorrectTabTitle"); + yield extension.unload(); +}); + +add_task(function* testDuplicatePinnedTab() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/"); + gBrowser.pinTab(tab); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + lastFocusedWindow: true, + }, function(tabs) { + // Duplicate the pinned tab, example.net. + browser.tabs.duplicate(tabs[0].id, (tab) => { + browser.test.assertEq("http://example.net/", tab.url); + // Should be the second tab, next to the one duplicated. + browser.test.assertEq(1, tab.index); + // Should be pinned. + browser.test.assertTrue(tab.pinned); + browser.test.notifyPass("tabs.duplicate.pinned"); + }); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.duplicate.pinned"); + yield extension.unload(); + + while (gBrowser.tabs[0].linkedBrowser.currentURI.spec === "http://example.net/") { + yield BrowserTestUtils.removeTab(gBrowser.tabs[0]); + } +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_events.js b/browser/components/extensions/test/browser/browser_ext_tabs_events.js new file mode 100644 index 000000000..75dea40fd --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_events.js @@ -0,0 +1,280 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testTabEvents() { + async function background() { + let events = []; + browser.tabs.onCreated.addListener(tab => { + events.push({type: "onCreated", tab}); + }); + + browser.tabs.onAttached.addListener((tabId, info) => { + events.push(Object.assign({type: "onAttached", tabId}, info)); + }); + + browser.tabs.onDetached.addListener((tabId, info) => { + events.push(Object.assign({type: "onDetached", tabId}, info)); + }); + + browser.tabs.onRemoved.addListener((tabId, info) => { + events.push(Object.assign({type: "onRemoved", tabId}, info)); + }); + + browser.tabs.onMoved.addListener((tabId, info) => { + events.push(Object.assign({type: "onMoved", tabId}, info)); + }); + + async function expectEvents(names) { + browser.test.log(`Expecting events: ${names.join(", ")}`); + + await new Promise(resolve => setTimeout(resolve, 0)); + + browser.test.assertEq(names.length, events.length, "Got expected number of events"); + for (let [i, name] of names.entries()) { + browser.test.assertEq(name, i in events && events[i].type, + `Got expected ${name} event`); + } + return events.splice(0); + } + + try { + browser.test.log("Create second browser window"); + + let windows = await Promise.all([ + browser.windows.getCurrent(), + browser.windows.create({url: "about:blank"}), + ]); + + let windowId = windows[0].id; + let otherWindowId = windows[1].id; + + let [created] = await expectEvents(["onCreated"]); + let initialTab = created.tab; + + + browser.test.log("Create tab in window 1"); + let tab = await browser.tabs.create({windowId, index: 0, url: "about:blank"}); + let oldIndex = tab.index; + browser.test.assertEq(0, oldIndex, "Tab has the expected index"); + + [created] = await expectEvents(["onCreated"]); + browser.test.assertEq(tab.id, created.tab.id, "Got expected tab ID"); + browser.test.assertEq(oldIndex, created.tab.index, "Got expected tab index"); + + + browser.test.log("Move tab to window 2"); + await browser.tabs.move([tab.id], {windowId: otherWindowId, index: 0}); + + let [detached, attached] = await expectEvents(["onDetached", "onAttached"]); + browser.test.assertEq(oldIndex, detached.oldPosition, "Expected old index"); + browser.test.assertEq(windowId, detached.oldWindowId, "Expected old window ID"); + + browser.test.assertEq(0, attached.newPosition, "Expected new index"); + browser.test.assertEq(otherWindowId, attached.newWindowId, "Expected new window ID"); + + + browser.test.log("Move tab within the same window"); + let [moved] = await browser.tabs.move([tab.id], {index: 1}); + browser.test.assertEq(1, moved.index, "Expected new index"); + + [moved] = await expectEvents(["onMoved"]); + browser.test.assertEq(tab.id, moved.tabId, "Expected tab ID"); + browser.test.assertEq(0, moved.fromIndex, "Expected old index"); + browser.test.assertEq(1, moved.toIndex, "Expected new index"); + browser.test.assertEq(otherWindowId, moved.windowId, "Expected window ID"); + + + browser.test.log("Remove tab"); + await browser.tabs.remove(tab.id); + let [removed] = await expectEvents(["onRemoved"]); + + browser.test.assertEq(tab.id, removed.tabId, "Expected removed tab ID"); + browser.test.assertEq(otherWindowId, removed.windowId, "Expected removed tab window ID"); + // Note: We want to test for the actual boolean value false here. + browser.test.assertEq(false, removed.isWindowClosing, "Expected isWindowClosing value"); + + + browser.test.log("Close second window"); + await browser.windows.remove(otherWindowId); + [removed] = await expectEvents(["onRemoved"]); + browser.test.assertEq(initialTab.id, removed.tabId, "Expected removed tab ID"); + browser.test.assertEq(otherWindowId, removed.windowId, "Expected removed tab window ID"); + browser.test.assertEq(true, removed.isWindowClosing, "Expected isWindowClosing value"); + + + browser.test.log("Create additional tab in window 1"); + tab = await browser.tabs.create({windowId, url: "about:blank"}); + await expectEvents(["onCreated"]); + + + browser.test.log("Create a new window, adopting the new tab"); + // We have to explicitly wait for the event here, since its timing is + // not predictable. + let promiseAttached = new Promise(resolve => { + browser.tabs.onAttached.addListener(function listener(tabId) { + browser.tabs.onAttached.removeListener(listener); + resolve(); + }); + }); + + let [window] = await Promise.all([ + browser.windows.create({tabId: tab.id}), + promiseAttached, + ]); + + [detached, attached] = await expectEvents(["onDetached", "onAttached"]); + + browser.test.assertEq(tab.id, detached.tabId, "Expected onDetached tab ID"); + + browser.test.assertEq(tab.id, attached.tabId, "Expected onAttached tab ID"); + browser.test.assertEq(0, attached.newPosition, "Expected onAttached new index"); + browser.test.assertEq(window.id, attached.newWindowId, + "Expected onAttached new window id"); + + browser.test.log("Close the new window"); + await browser.windows.remove(window.id); + + browser.test.notifyPass("tabs-events"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tabs-events"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs-events"); + yield extension.unload(); +}); + +add_task(function* testTabEventsSize() { + function background() { + function sendSizeMessages(tab, type) { + browser.test.sendMessage(`${type}-dims`, {width: tab.width, height: tab.height}); + } + + browser.tabs.onCreated.addListener(tab => { + sendSizeMessages(tab, "on-created"); + }); + + browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status == "complete") { + sendSizeMessages(tab, "on-updated"); + } + }); + + browser.test.onMessage.addListener(async (msg, arg) => { + if (msg === "create-tab") { + let tab = await browser.tabs.create({url: "http://example.com/"}); + sendSizeMessages(tab, "create"); + browser.test.sendMessage("created-tab-id", tab.id); + } else if (msg === "update-tab") { + let tab = await browser.tabs.update(arg, {url: "http://example.org/"}); + sendSizeMessages(tab, "update"); + } else if (msg === "remove-tab") { + browser.tabs.remove(arg); + browser.test.sendMessage("tab-removed"); + } + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + background, + }); + + const RESOLUTION_PREF = "layout.css.devPixelsPerPx"; + registerCleanupFunction(() => { + SpecialPowers.clearUserPref(RESOLUTION_PREF); + }); + + function checkDimensions(dims, type) { + is(dims.width, gBrowser.selectedBrowser.clientWidth, `tab from ${type} reports expected width`); + is(dims.height, gBrowser.selectedBrowser.clientHeight, `tab from ${type} reports expected height`); + } + + yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + for (let resolution of [2, 1]) { + SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution)); + is(window.devicePixelRatio, resolution, "window has the required resolution"); + + extension.sendMessage("create-tab"); + let tabId = yield extension.awaitMessage("created-tab-id"); + + checkDimensions(yield extension.awaitMessage("create-dims"), "create"); + checkDimensions(yield extension.awaitMessage("on-created-dims"), "onCreated"); + checkDimensions(yield extension.awaitMessage("on-updated-dims"), "onUpdated"); + + extension.sendMessage("update-tab", tabId); + + checkDimensions(yield extension.awaitMessage("update-dims"), "update"); + checkDimensions(yield extension.awaitMessage("on-updated-dims"), "onUpdated"); + + extension.sendMessage("remove-tab", tabId); + yield extension.awaitMessage("tab-removed"); + } + + yield extension.unload(); + SpecialPowers.clearUserPref(RESOLUTION_PREF); +}); + +add_task(function* testTabRemovalEvent() { + async function background() { + function awaitLoad(tabId) { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) { + if (tabId == tabId_ && changed.status == "complete") { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + } + + chrome.tabs.onRemoved.addListener((tabId, info) => { + browser.test.log("Make sure the removed tab is not available in the tabs.query callback."); + chrome.tabs.query({}, tabs => { + for (let tab of tabs) { + browser.test.assertTrue(tab.id != tabId, "Tab query should not include removed tabId"); + } + browser.test.notifyPass("tabs-events"); + }); + }); + + try { + let url = "http://example.com/browser/browser/components/extensions/test/browser/context.html"; + let tab = await browser.tabs.create({url: url}); + await awaitLoad(tab.id); + + await browser.tabs.remove(tab.id); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tabs-events"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs-events"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js new file mode 100644 index 000000000..5a15f2e39 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js @@ -0,0 +1,234 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testExecuteScript() { + let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {}); + + function countMM(messageManagerMap) { + let count = 0; + // List of permanent message managers in the main process. We should not + // count them in the test because MessageChannel unsubscribes when the + // message manager closes, which never happens to these, of course. + let globalMMs = [ + Services.mm, + Services.ppmm, + Services.ppmm.getChildAt(0), + ]; + for (let mm of messageManagerMap.keys()) { + // Sanity check: mm is a message manager. + try { + mm.QueryInterface(Ci.nsIMessageSender); + } catch (e) { + mm.QueryInterface(Ci.nsIMessageBroadcaster); + } + if (!globalMMs.includes(mm)) { + ++count; + } + } + return count; + } + + let messageManagersSize = countMM(MessageChannel.messageManagers); + let responseManagersSize = countMM(MessageChannel.responseManagers); + + const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/"; + const URL = BASE + "file_iframe_document.html"; + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true); + + async function background() { + try { + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + let frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); + + browser.test.log(`FRAMES: ${frames[1].frameId} ${JSON.stringify(frames)}\n`); + await Promise.all([ + browser.tabs.executeScript({ + code: "42", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one callback result"); + browser.test.assertEq(42, result[0], "Expected callback result"); + }), + + browser.tabs.executeScript({ + file: "script.js", + code: "42", + }).then(result => { + browser.test.fail("Expected not to be able to execute a script with both file and code"); + }, error => { + browser.test.assertTrue(/a 'code' or a 'file' property, but not both/.test(error.message), + "Got expected error"); + }), + + browser.tabs.executeScript({ + file: "script.js", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one callback result"); + browser.test.assertEq(undefined, result[0], "Expected callback result"); + }), + + browser.tabs.executeScript({ + file: "script2.js", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one callback result"); + browser.test.assertEq(27, result[0], "Expected callback result"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + allFrames: true, + }).then(result => { + browser.test.assertTrue(Array.isArray(result), "Result is an array"); + + browser.test.assertEq(2, result.length, "Result has correct length"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "First result is correct"); + browser.test.assertEq("http://mochi.test:8888/", result[1], "Second result is correct"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + runAt: "document_end", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected callback result"); + browser.test.assertEq("string", typeof result[0], "Result is a string"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "Result is correct"); + }), + + browser.tabs.executeScript({ + code: "window", + }).then(result => { + browser.test.fail("Expected error when returning non-structured-clonable object"); + }, error => { + browser.test.assertEq("Script returned non-structured-clonable data", + error.message, "Got expected error"); + }), + + browser.tabs.executeScript({ + code: "Promise.resolve(window)", + }).then(result => { + browser.test.fail("Expected error when returning non-structured-clonable object"); + }, error => { + browser.test.assertEq("Script returned non-structured-clonable data", + error.message, "Got expected error"); + }), + + browser.tabs.executeScript({ + frameId: Number.MAX_SAFE_INTEGER, + code: "42", + }).then(result => { + browser.test.fail("Expected error when specifying invalid frame ID"); + }, error => { + let details = { + frame_id: Number.MAX_SAFE_INTEGER, + matchesHost: ["http://mochi.test/", "http://example.com/"], + }; + browser.test.assertEq(`No window matching ${JSON.stringify(details)}`, + error.message, "Got expected error"); + }), + + browser.tabs.create({url: "http://example.net/", active: false}).then(async tab => { + await browser.tabs.executeScript(tab.id, { + code: "42", + }).then(result => { + browser.test.fail("Expected error when trying to execute on invalid domain"); + }, error => { + let details = { + matchesHost: ["http://mochi.test/", "http://example.com/"], + }; + browser.test.assertEq(`No window matching ${JSON.stringify(details)}`, + error.message, "Got expected error"); + }); + + await browser.tabs.remove(tab.id); + }), + + browser.tabs.executeScript({ + code: "Promise.resolve(42)", + }).then(result => { + browser.test.assertEq(42, result[0], "Got expected promise resolution value as result"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + runAt: "document_end", + allFrames: true, + }).then(result => { + browser.test.assertTrue(Array.isArray(result), "Result is an array"); + + browser.test.assertEq(2, result.length, "Result has correct length"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "First result is correct"); + browser.test.assertEq("http://mochi.test:8888/", result[1], "Second result is correct"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + frameId: frames[0].frameId, + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one result"); + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), `Result for frameId[0] is correct: ${result[0]}`); + }), + + browser.tabs.executeScript({ + code: "location.href;", + frameId: frames[1].frameId, + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one result"); + browser.test.assertEq("http://mochi.test:8888/", result[0], "Result for frameId[1] is correct"); + }), + + browser.tabs.create({url: "http://example.com/"}).then(async tab => { + let result = await browser.tabs.executeScript(tab.id, {code: "location.href"}); + + browser.test.assertEq("http://example.com/", result[0], "Script executed correctly in new tab"); + + await browser.tabs.remove(tab.id); + }), + + new Promise(resolve => { + browser.runtime.onMessage.addListener(message => { + browser.test.assertEq("script ran", message, "Expected runtime message"); + resolve(); + }); + }), + ]); + + browser.test.notifyPass("executeScript"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("executeScript"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/", "http://example.com/", "webNavigation"], + }, + + background, + + files: { + "script.js": function() { + browser.runtime.sendMessage("script ran"); + }, + + "script2.js": "27", + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("executeScript"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); + + // Make sure that we're not holding on to references to closed message + // managers. + is(countMM(MessageChannel.messageManagers), messageManagersSize, "Message manager count"); + is(countMM(MessageChannel.responseManagers), responseManagersSize, "Response manager count"); + is(MessageChannel.pendingResponses.size, 0, "Pending response count"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js new file mode 100644 index 000000000..d11354ead --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js @@ -0,0 +1,217 @@ +"use strict"; + +// This is a pretty terrible hack, but it's the best we can do until we +// support |executeScript| callbacks and |lastError|. +function* testHasNoPermission(params) { + let contentSetup = params.contentSetup || (() => Promise.resolve()); + + async function background(contentSetup) { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.assertEq(msg, "second script ran", "second script ran"); + browser.test.notifyPass("executeScript"); + }); + + browser.test.onMessage.addListener(msg => { + browser.test.assertEq(msg, "execute-script"); + + browser.tabs.query({currentWindow: true}, tabs => { + browser.tabs.executeScript({ + file: "script.js", + }); + + // Execute a script we know we have permissions for in the + // second tab, in the hopes that it will execute after the + // first one. This has intermittent failure written all over + // it, but it's just about the best we can do until we + // support callbacks for executeScript. + browser.tabs.executeScript(tabs[1].id, { + file: "second-script.js", + }); + }); + }); + + await contentSetup(); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: params.manifest, + + background: `(${background})(${contentSetup})`, + + files: { + "script.js": function() { + browser.runtime.sendMessage("first script ran"); + }, + + "second-script.js": function() { + browser.runtime.sendMessage("second script ran"); + }, + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + if (params.setup) { + yield params.setup(extension); + } + + extension.sendMessage("execute-script"); + + yield extension.awaitFinish("executeScript"); + yield extension.unload(); +} + +add_task(function* testBadPermissions() { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/"); + + info("Test no special permissions"); + yield testHasNoPermission({ + manifest: {"permissions": ["http://example.com/"]}, + }); + + info("Test tabs permissions"); + yield testHasNoPermission({ + manifest: {"permissions": ["http://example.com/", "tabs"]}, + }); + + info("Test no special permissions, commands, key press"); + yield testHasNoPermission({ + manifest: { + "permissions": ["http://example.com/"], + "commands": { + "test-tabs-executeScript": { + "suggested_key": { + "default": "Alt+Shift+K", + }, + }, + }, + }, + contentSetup() { + browser.commands.onCommand.addListener(function(command) { + if (command == "test-tabs-executeScript") { + browser.test.sendMessage("tabs-command-key-pressed"); + } + }); + return Promise.resolve(); + }, + setup: function* (extension) { + yield EventUtils.synthesizeKey("k", {altKey: true, shiftKey: true}); + yield extension.awaitMessage("tabs-command-key-pressed"); + }, + }); + + info("Test active tab, commands, no key press"); + yield testHasNoPermission({ + manifest: { + "permissions": ["http://example.com/", "activeTab"], + "commands": { + "test-tabs-executeScript": { + "suggested_key": { + "default": "Alt+Shift+K", + }, + }, + }, + }, + }); + + info("Test active tab, browser action, no click"); + yield testHasNoPermission({ + manifest: { + "permissions": ["http://example.com/", "activeTab"], + "browser_action": {}, + }, + }); + + info("Test active tab, page action, no click"); + yield testHasNoPermission({ + manifest: { + "permissions": ["http://example.com/", "activeTab"], + "page_action": {}, + }, + async contentSetup() { + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + await browser.pageAction.show(tab.id); + }, + }); + + yield BrowserTestUtils.removeTab(tab2); + yield BrowserTestUtils.removeTab(tab1); +}); + +add_task(function* testBadURL() { + async function background() { + let promises = [ + new Promise(resolve => { + browser.tabs.executeScript({ + file: "http://example.com/script.js", + }, result => { + browser.test.assertEq(undefined, result, "Result value"); + + browser.test.assertTrue(browser.extension.lastError instanceof Error, + "runtime.lastError is Error"); + + browser.test.assertTrue(browser.runtime.lastError instanceof Error, + "runtime.lastError is Error"); + + browser.test.assertEq( + "Files to be injected must be within the extension", + browser.extension.lastError && browser.extension.lastError.message, + "extension.lastError value"); + + browser.test.assertEq( + "Files to be injected must be within the extension", + browser.runtime.lastError && browser.runtime.lastError.message, + "runtime.lastError value"); + + resolve(); + }); + }), + + browser.tabs.executeScript({ + file: "http://example.com/script.js", + }).catch(error => { + browser.test.assertTrue(error instanceof Error, "Error is Error"); + + browser.test.assertEq(null, browser.extension.lastError, + "extension.lastError value"); + + browser.test.assertEq(null, browser.runtime.lastError, + "runtime.lastError value"); + + browser.test.assertEq( + "Files to be injected must be within the extension", + error && error.message, + "error value"); + }), + ]; + + await Promise.all(promises); + + browser.test.notifyPass("executeScript-lastError"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["<all_urls>"], + }, + + background, + }); + + yield extension.startup(); + + yield extension.awaitFinish("executeScript-lastError"); + + yield extension.unload(); +}); + +// TODO: Test that |executeScript| fails if the tab has navigated to a +// new page, and no longer matches our expected state. This involves +// intentionally trying to trigger a race condition, and is probably not +// even worth attempting until we have proper |executeScript| callbacks. + +add_task(forceGC); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js new file mode 100644 index 000000000..cf4721310 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js @@ -0,0 +1,189 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +requestLongerTimeout(2); + +function* testHasPermission(params) { + let contentSetup = params.contentSetup || (() => Promise.resolve()); + + async function background(contentSetup) { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.assertEq(msg, "script ran", "script ran"); + browser.test.notifyPass("executeScript"); + }); + + browser.test.onMessage.addListener(msg => { + browser.test.assertEq(msg, "execute-script"); + + browser.tabs.executeScript({ + file: "script.js", + }); + }); + + await contentSetup(); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: params.manifest, + + background: `(${background})(${contentSetup})`, + + files: { + "script.js": function() { + browser.runtime.sendMessage("script ran"); + }, + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + if (params.setup) { + yield params.setup(extension); + } + + extension.sendMessage("execute-script"); + + yield extension.awaitFinish("executeScript"); + + if (params.tearDown) { + yield params.tearDown(extension); + } + + yield extension.unload(); +} + +add_task(function* testGoodPermissions() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true); + + info("Test explicit host permission"); + yield testHasPermission({ + manifest: {"permissions": ["http://mochi.test/"]}, + }); + + info("Test explicit host subdomain permission"); + yield testHasPermission({ + manifest: {"permissions": ["http://*.mochi.test/"]}, + }); + + info("Test explicit <all_urls> permission"); + yield testHasPermission({ + manifest: {"permissions": ["<all_urls>"]}, + }); + + info("Test activeTab permission with a command key press"); + yield testHasPermission({ + manifest: { + "permissions": ["activeTab"], + "commands": { + "test-tabs-executeScript": { + "suggested_key": { + "default": "Alt+Shift+K", + }, + }, + }, + }, + contentSetup() { + browser.commands.onCommand.addListener(function(command) { + if (command == "test-tabs-executeScript") { + browser.test.sendMessage("tabs-command-key-pressed"); + } + }); + return Promise.resolve(); + }, + setup: function* (extension) { + yield EventUtils.synthesizeKey("k", {altKey: true, shiftKey: true}); + yield extension.awaitMessage("tabs-command-key-pressed"); + }, + }); + + info("Test activeTab permission with a browser action click"); + yield testHasPermission({ + manifest: { + "permissions": ["activeTab"], + "browser_action": {}, + }, + contentSetup() { + browser.browserAction.onClicked.addListener(() => { + browser.test.log("Clicked."); + }); + return Promise.resolve(); + }, + setup: clickBrowserAction, + tearDown: closeBrowserAction, + }); + + info("Test activeTab permission with a page action click"); + yield testHasPermission({ + manifest: { + "permissions": ["activeTab"], + "page_action": {}, + }, + contentSetup: async () => { + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + await browser.pageAction.show(tab.id); + }, + setup: clickPageAction, + tearDown: closePageAction, + }); + + info("Test activeTab permission with a browser action w/popup click"); + yield testHasPermission({ + manifest: { + "permissions": ["activeTab"], + "browser_action": {"default_popup": "_blank.html"}, + }, + setup: async extension => { + await clickBrowserAction(extension); + return awaitExtensionPanel(extension, window, "_blank.html"); + }, + tearDown: closeBrowserAction, + }); + + info("Test activeTab permission with a page action w/popup click"); + yield testHasPermission({ + manifest: { + "permissions": ["activeTab"], + "page_action": {"default_popup": "_blank.html"}, + }, + contentSetup: async () => { + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + await browser.pageAction.show(tab.id); + }, + setup: clickPageAction, + tearDown: closePageAction, + }); + + info("Test activeTab permission with a context menu click"); + yield testHasPermission({ + manifest: { + "permissions": ["activeTab", "contextMenus"], + }, + contentSetup() { + browser.contextMenus.create({title: "activeTab", contexts: ["all"]}); + return Promise.resolve(); + }, + setup: function* (extension) { + let contextMenu = document.getElementById("contentAreaContextMenu"); + let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); + + yield BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {type: "contextmenu", button: 2}, + gBrowser.selectedBrowser); + yield awaitPopupShown; + + let item = contextMenu.querySelector("[label=activeTab]"); + + yield EventUtils.synthesizeMouseAtCenter(item, {}, window); + + yield awaitPopupHidden; + }, + }); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(forceGC); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js new file mode 100644 index 000000000..7b2ffe175 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js @@ -0,0 +1,67 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testExecuteScriptAtOnUpdated() { + const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/"; + const URL = BASE + "file_iframe_document.html"; + // This is a regression test for bug 1325830. + // The bug (executeScript not completing any more) occurred when executeScript + // was called early at the onUpdated event, unless the tabs.create method is + // called. So this test does not use tabs.create to open new tabs. + // Note that if this test is run together with other tests that do call + // tabs.create, then this test case does not properly test the conditions of + // the regression any more. To verify that the regression has been resolved, + // this test must be run in isolation. + + function background() { + // Using variables to prevent listeners from running more than once, instead + // of removing the listener. This is to minimize any IPC, since the bug that + // is being tested is sensitive to timing. + let ignore = false; + let url; + browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === "loading" && tab.url === url && !ignore) { + ignore = true; + browser.tabs.executeScript(tabId, { + code: "document.URL", + }).then(results => { + browser.test.assertEq(url, results[0], "Content script should run"); + browser.test.notifyPass("executeScript-at-onUpdated"); + }, error => { + browser.test.fail(`Unexpected error: ${error} :: ${error.stack}`); + browser.test.notifyFail("executeScript-at-onUpdated"); + }); + // (running this log call after executeScript to minimize IPC between + // onUpdated and executeScript.) + browser.test.log(`Found expected navigation to ${url}`); + } else { + // The bug occurs when executeScript is called before a tab is + // initialized. + browser.tabs.executeScript(tabId, {code: ""}); + } + }); + browser.test.onMessage.addListener(testUrl => { + url = testUrl; + browser.test.sendMessage("open-test-tab"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/", "tabs"], + }, + background, + }); + + yield extension.startup(); + extension.sendMessage(URL); + yield extension.awaitMessage("open-test-tab"); + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true); + + yield extension.awaitFinish("executeScript-at-onUpdated"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js new file mode 100644 index 000000000..a4c0ed6f1 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js @@ -0,0 +1,107 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/** + * These tests ensure that the runAt argument to tabs.executeScript delays + * script execution until the document has reached the correct state. + * + * Since tests of this nature are especially race-prone, it relies on a + * server-JS script to delay the completion of our test page's load cycle long + * enough for us to attempt to load our scripts in the earlies phase we support. + * + * And since we can't actually rely on that timing, it retries any attempts that + * fail to load as early as expected, but don't load at any illegal time. + */ + +add_task(function* testExecuteScript() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", true); + + async function background() { + let tab; + + const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/"; + const URL = BASE + "file_iframe_document.sjs"; + + const MAX_TRIES = 10; + + try { + [tab] = await browser.tabs.query({active: true, currentWindow: true}); + + let success = false; + for (let tries = 0; !success && tries < MAX_TRIES; tries++) { + let url = `${URL}?r=${Math.random()}`; + + let loadingPromise = new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId, changed, tab_) { + if (tabId == tab.id && changed.status == "loading" && tab_.url == url) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + + // TODO: Test allFrames and frameId. + + await browser.tabs.update({url}); + await loadingPromise; + + let states = await Promise.all([ + // Send the executeScript requests in the reverse order that we expect + // them to execute in, to avoid them passing only because of timing + // races. + browser.tabs.executeScript({ + code: "document.readyState", + runAt: "document_idle", + }), + browser.tabs.executeScript({ + code: "document.readyState", + runAt: "document_end", + }), + browser.tabs.executeScript({ + code: "document.readyState", + runAt: "document_start", + }), + ].reverse()); + + browser.test.log(`Got states: ${states}`); + + // Make sure that none of our scripts executed earlier than expected, + // regardless of retries. + browser.test.assertTrue(states[1] == "interactive" || states[1] == "complete", + `document_end state is valid: ${states[1]}`); + browser.test.assertTrue(states[2] == "complete", + `document_idle state is valid: ${states[2]}`); + + // If we have the earliest valid states for each script, we're done. + // Otherwise, try again. + success = (states[0] == "loading" && + states[1] == "interactive" && + states[2] == "complete"); + } + + browser.test.assertTrue(success, "Got the earliest expected states at least once"); + + browser.test.notifyPass("executeScript-runAt"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("executeScript-runAt"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/", "tabs"], + }, + + background, + }); + + yield extension.startup(); + + yield extension.awaitFinish("executeScript-runAt"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js b/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js new file mode 100644 index 000000000..b67d935cb --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js @@ -0,0 +1,70 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + + "browser_action": {"default_popup": "popup.html"}, + }, + + files: { + "tab.js": function() { + let url = document.location.href; + + browser.tabs.getCurrent(currentTab => { + browser.test.assertEq(currentTab.url, url, "getCurrent in non-active background tab"); + + // Activate the tab. + browser.tabs.onActivated.addListener(function listener({tabId}) { + if (tabId == currentTab.id) { + browser.tabs.onActivated.removeListener(listener); + + browser.tabs.getCurrent(currentTab => { + browser.test.assertEq(currentTab.id, tabId, "in active background tab"); + browser.test.assertEq(currentTab.url, url, "getCurrent in non-active background tab"); + + browser.test.sendMessage("tab-finished"); + }); + } + }); + browser.tabs.update(currentTab.id, {active: true}); + }); + }, + + "popup.js": function() { + browser.tabs.getCurrent(tab => { + browser.test.assertEq(tab, undefined, "getCurrent in popup script"); + browser.test.sendMessage("popup-finished"); + }); + }, + + "tab.html": `<head><meta charset="utf-8"><script src="tab.js"></script></head>`, + "popup.html": `<head><meta charset="utf-8"><script src="popup.js"></script></head>`, + }, + + background: function() { + browser.tabs.getCurrent(tab => { + browser.test.assertEq(tab, undefined, "getCurrent in background script"); + browser.test.sendMessage("background-finished"); + }); + + browser.tabs.create({url: "tab.html", active: false}); + }, + }); + + yield extension.startup(); + + yield extension.awaitMessage("background-finished"); + yield extension.awaitMessage("tab-finished"); + + clickBrowserAction(extension); + yield awaitExtensionPanel(extension); + yield extension.awaitMessage("popup-finished"); + yield closeBrowserAction(extension); + + // The extension tab is automatically closed when the extension unloads. + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js b/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js new file mode 100644 index 000000000..a8e172d94 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js @@ -0,0 +1,86 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testExecuteScript() { + let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {}); + + let messageManagersSize = MessageChannel.messageManagers.size; + let responseManagersSize = MessageChannel.responseManagers.size; + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true); + + async function background() { + let tasks = [ + { + background: "transparent", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.insertCSS({ + file: "file2.css", + }); + }, + }, + { + background: "rgb(42, 42, 42)", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.insertCSS({ + code: "* { background: rgb(42, 42, 42) }", + }); + }, + }, + ]; + + function checkCSS() { + let computedStyle = window.getComputedStyle(document.body); + return [computedStyle.backgroundColor, computedStyle.color]; + } + + try { + for (let {promise, background, foreground} of tasks) { + let result = await promise(); + + browser.test.assertEq(undefined, result, "Expected callback result"); + + [result] = await browser.tabs.executeScript({ + code: `(${checkCSS})()`, + }); + + browser.test.assertEq(background, result[0], "Expected background color"); + browser.test.assertEq(foreground, result[1], "Expected foreground color"); + } + + browser.test.notifyPass("insertCSS"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFailure("insertCSS"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/"], + }, + + background, + + files: { + "file2.css": "* { color: rgb(0, 113, 4) }", + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("insertCSS"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); + + // Make sure that we're not holding on to references to closed message + // managers. + is(MessageChannel.messageManagers.size, messageManagersSize, "Message manager count"); + is(MessageChannel.responseManagers.size, responseManagersSize, "Response manager count"); + is(MessageChannel.pendingResponses.size, 0, "Pending response count"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move.js b/browser/components/extensions/test/browser/browser_ext_tabs_move.js new file mode 100644 index 000000000..917cdc146 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_move.js @@ -0,0 +1,103 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:config"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: async function() { + let [tab] = await browser.tabs.query({lastFocusedWindow: true}); + + browser.tabs.move(tab.id, {index: 0}); + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + + browser.test.assertEq(tabs[0].url, tab.url, "should be first tab"); + browser.test.notifyPass("tabs.move.single"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.single"); + yield extension.unload(); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: async function() { + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + + tabs.sort(function(a, b) { return a.url > b.url; }); + + browser.tabs.move(tabs.map(tab => tab.id), {index: 0}); + + tabs = await browser.tabs.query({lastFocusedWindow: true}); + + browser.test.assertEq(tabs[0].url, "about:blank", "should be first tab"); + browser.test.assertEq(tabs[1].url, "about:config", "should be second tab"); + browser.test.assertEq(tabs[2].url, "about:robots", "should be third tab"); + + browser.test.notifyPass("tabs.move.multiple"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.multiple"); + yield extension.unload(); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + async background() { + let [, tab] = await browser.tabs.query({lastFocusedWindow: true}); + + // Assuming that tab.id of 12345 does not exist. + await browser.test.assertRejects( + browser.tabs.move([tab.id, 12345], {index: 0}), + /Invalid tab/, + "Should receive invalid tab error"); + + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + browser.test.assertEq(tabs[1].url, tab.url, "should be second tab"); + browser.test.notifyPass("tabs.move.invalid"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.invalid"); + yield extension.unload(); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: async function() { + let [tab] = await browser.tabs.query({lastFocusedWindow: true}); + browser.tabs.move(tab.id, {index: -1}); + + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + + browser.test.assertEq(tabs[2].url, tab.url, "should be last tab"); + browser.test.notifyPass("tabs.move.last"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.last"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js new file mode 100644 index 000000000..f3bce364a --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js @@ -0,0 +1,98 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/"); + let window1 = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.com/"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + async background() { + let tabs = await browser.tabs.query({url: "<all_urls>"}); + let destination = tabs[0]; + let source = tabs[1]; // skip over about:blank in window1 + + // Assuming that this windowId does not exist. + await browser.test.assertRejects( + browser.tabs.move(source.id, {windowId: 123144576, index: 0}), + /Invalid window/, + "Should receive invalid window error"); + + browser.tabs.move(source.id, {windowId: destination.windowId, index: 0}); + + tabs = await browser.tabs.query({url: "<all_urls>"}); + browser.test.assertEq(tabs[0].url, "http://example.com/"); + browser.test.assertEq(tabs[0].windowId, destination.windowId); + browser.test.notifyPass("tabs.move.window"); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.window"); + yield extension.unload(); + + for (let tab of window.gBrowser.tabs) { + yield BrowserTestUtils.removeTab(tab); + } + yield BrowserTestUtils.closeWindow(window1); +}); + +add_task(function* test_currentWindowAfterTabMoved() { + const files = { + "current.html": "<meta charset=utf-8><script src=current.js></script>", + "current.js": function() { + browser.test.onMessage.addListener(msg => { + if (msg === "current") { + browser.windows.getCurrent(win => { + browser.test.sendMessage("id", win.id); + }); + } + }); + browser.test.sendMessage("ready"); + }, + }; + + async function background() { + let tabId; + + const url = browser.extension.getURL("current.html"); + + browser.test.onMessage.addListener(async msg => { + if (msg === "move") { + await browser.windows.create({tabId}); + browser.test.sendMessage("moved"); + } else if (msg === "close") { + await browser.tabs.remove(tabId); + browser.test.sendMessage("done"); + } + }); + + let tab = await browser.tabs.create({url}); + tabId = tab.id; + } + + const extension = ExtensionTestUtils.loadExtension({files, background}); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("current"); + const first = yield extension.awaitMessage("id"); + + extension.sendMessage("move"); + yield extension.awaitMessage("moved"); + + extension.sendMessage("current"); + const second = yield extension.awaitMessage("id"); + + isnot(first, second, "current window id is different after moving the tab"); + + extension.sendMessage("close"); + yield extension.awaitMessage("done"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js new file mode 100644 index 000000000..dacd547f2 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js @@ -0,0 +1,43 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let window1 = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, "http://example.net/"); + yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, "http://example.com/"); + yield BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.net/"); + yield BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.com/"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query( + {url: "<all_urls>"}, + tabs => { + let move1 = tabs[1]; + let move3 = tabs[3]; + browser.tabs.move([move1.id, move3.id], {index: 0}); + browser.tabs.query( + {url: "<all_urls>"}, + tabs => { + browser.test.assertEq(tabs[0].url, move1.url); + browser.test.assertEq(tabs[2].url, move3.url); + browser.test.notifyPass("tabs.move.multiple"); + }); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.multiple"); + yield extension.unload(); + + for (let tab of window.gBrowser.tabs) { + yield BrowserTestUtils.removeTab(tab); + } + yield BrowserTestUtils.closeWindow(window1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js new file mode 100644 index 000000000..c592dc56d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js @@ -0,0 +1,42 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/"); + let window1 = yield BrowserTestUtils.openNewBrowserWindow(); + let tab1 = yield BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.com/"); + window1.gBrowser.pinTab(tab1); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query( + {url: "<all_urls>"}, + tabs => { + let destination = tabs[0]; + let source = tabs[1]; // remember, pinning moves it to the left. + browser.tabs.move(source.id, {windowId: destination.windowId, index: 0}); + + browser.tabs.query( + {url: "<all_urls>"}, + tabs => { + browser.test.assertEq(true, tabs[0].pinned); + browser.test.notifyPass("tabs.move.pin"); + }); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.move.pin"); + yield extension.unload(); + + for (let tab of window.gBrowser.tabs) { + yield BrowserTestUtils.removeTab(tab); + } + yield BrowserTestUtils.closeWindow(window1); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js new file mode 100644 index 000000000..9cc2554d6 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js @@ -0,0 +1,126 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testTabEvents() { + async function background() { + /** The list of active tab ID's */ + let tabIds = []; + + /** + * Stores the events that fire for each tab. + * + * events { + * tabId1: [event1, event2, ...], + * tabId2: [event1, event2, ...], + * } + */ + let events = {}; + + browser.tabs.onActivated.addListener((info) => { + if (info.tabId in events) { + events[info.tabId].push("onActivated"); + } else { + events[info.tabId] = ["onActivated"]; + } + }); + + browser.tabs.onHighlighted.addListener((info) => { + if (info.tabIds[0] in events) { + events[info.tabIds[0]].push("onHighlighted"); + } else { + events[info.tabIds[0]] = ["onHighlighted"]; + } + }); + + /** + * Asserts that the expected events are fired for the tab with id = tabId. + * The events associated to the specified tab are removed after this check is made. + * + * @param {number} tabId + * @param {Array<string>} expectedEvents + */ + async function expectEvents(tabId, expectedEvents) { + browser.test.log(`Expecting events: ${expectedEvents.join(", ")}`); + + await new Promise(resolve => setTimeout(resolve, 0)); + + browser.test.assertEq(expectedEvents.length, events[tabId].length, + `Got expected number of events for ${tabId}`); + + for (let [i, name] of expectedEvents.entries()) { + browser.test.assertEq(name, i in events[tabId] && events[tabId][i], + `Got expected ${name} event`); + } + delete events[tabId]; + } + + /** + * Opens a new tab and asserts that the correct events are fired. + * + * @param {number} windowId + */ + async function openTab(windowId) { + let tab = await browser.tabs.create({windowId}); + + tabIds.push(tab.id); + browser.test.log(`Opened tab ${tab.id}`); + + await expectEvents(tab.id, [ + "onActivated", + "onHighlighted", + ]); + } + + /** + * Highlights an existing tab and asserts that the correct events are fired. + * + * @param {number} tabId + */ + async function highlightTab(tabId) { + browser.test.log(`Highlighting tab ${tabId}`); + let tab = await browser.tabs.update(tabId, {active: true}); + + browser.test.assertEq(tab.id, tabId, `Tab ${tab.id} highlighted`); + + await expectEvents(tab.id, [ + "onActivated", + "onHighlighted", + ]); + } + + /** + * The main entry point to the tests. + */ + let tabs = await browser.tabs.query({active: true, currentWindow: true}); + + let activeWindow = tabs[0].windowId; + await Promise.all([ + openTab(activeWindow), + openTab(activeWindow), + openTab(activeWindow), + ]); + + await Promise.all([ + highlightTab(tabIds[0]), + highlightTab(tabIds[1]), + highlightTab(tabIds[2]), + ]); + + await Promise.all(tabIds.map(id => browser.tabs.remove(id))); + + browser.test.notifyPass("tabs.highlight"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.highlight"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js new file mode 100644 index 000000000..2c26bbd16 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js @@ -0,0 +1,198 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +requestLongerTimeout(2); + +add_task(function* () { + let win1 = yield BrowserTestUtils.openNewBrowserWindow(); + + yield focusWindow(win1); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/context_tabs_onUpdated_page.html"], + "js": ["content-script.js"], + "run_at": "document_start", + }], + }, + + background: function() { + let pageURL = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html"; + + let expectedSequence = [ + {status: "loading"}, + {status: "loading", url: pageURL}, + {status: "complete"}, + ]; + let collectedSequence = []; + + browser.tabs.onUpdated.addListener(function(tabId, updatedInfo) { + // onUpdated also fires with updatedInfo.faviconUrl, so explicitly + // check for updatedInfo.status before recording the event. + if ("status" in updatedInfo) { + collectedSequence.push(updatedInfo); + } + }); + + browser.runtime.onMessage.addListener(function() { + if (collectedSequence.length !== expectedSequence.length) { + browser.test.assertEq( + JSON.stringify(expectedSequence), + JSON.stringify(collectedSequence), + "got unexpected number of updateInfo data" + ); + } else { + for (let i = 0; i < expectedSequence.length; i++) { + browser.test.assertEq( + expectedSequence[i].status, + collectedSequence[i].status, + "check updatedInfo status" + ); + if (expectedSequence[i].url || collectedSequence[i].url) { + browser.test.assertEq( + expectedSequence[i].url, + collectedSequence[i].url, + "check updatedInfo url" + ); + } + } + } + + browser.test.notifyPass("tabs.onUpdated"); + }); + + browser.tabs.create({url: pageURL}); + }, + files: { + "content-script.js": ` + window.addEventListener("message", function(evt) { + if (evt.data == "frame-updated") { + browser.runtime.sendMessage("load-completed"); + } + }, true); + `, + }, + }); + + yield Promise.all([ + extension.startup(), + extension.awaitFinish("tabs.onUpdated"), + ]); + + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(win1); +}); + +function* do_test_update(background, withPermissions = true) { + let win1 = yield BrowserTestUtils.openNewBrowserWindow(); + + yield focusWindow(win1); + + let manifest = {}; + if (withPermissions) { + manifest.permissions = ["tabs"]; + } + let extension = ExtensionTestUtils.loadExtension({manifest, background}); + + yield Promise.all([ + yield extension.startup(), + yield extension.awaitFinish("finish"), + ]); + + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(win1); +} + +add_task(function* test_pinned() { + yield do_test_update(function background() { + // Create a new tab for testing update. + browser.tabs.create({}, function(tab) { + browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) { + // Check callback + browser.test.assertEq(tabId, tab.id, "Check tab id"); + browser.test.log("onUpdate: " + JSON.stringify(changeInfo)); + if ("pinned" in changeInfo) { + browser.test.assertTrue(changeInfo.pinned, "Check changeInfo.pinned"); + browser.tabs.onUpdated.removeListener(onUpdated); + // Remove created tab. + browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + return; + } + }); + browser.tabs.update(tab.id, {pinned: true}); + }); + }); +}); + +add_task(function* test_unpinned() { + yield do_test_update(function background() { + // Create a new tab for testing update. + browser.tabs.create({pinned: true}, function(tab) { + browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) { + // Check callback + browser.test.assertEq(tabId, tab.id, "Check tab id"); + browser.test.log("onUpdate: " + JSON.stringify(changeInfo)); + if ("pinned" in changeInfo) { + browser.test.assertFalse(changeInfo.pinned, "Check changeInfo.pinned"); + browser.tabs.onUpdated.removeListener(onUpdated); + // Remove created tab. + browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + return; + } + }); + browser.tabs.update(tab.id, {pinned: false}); + }); + }); +}); + +add_task(function* test_url() { + yield do_test_update(function background() { + // Create a new tab for testing update. + browser.tabs.create({}, function(tab) { + browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) { + // Check callback + browser.test.assertEq(tabId, tab.id, "Check tab id"); + browser.test.log("onUpdate: " + JSON.stringify(changeInfo)); + if ("url" in changeInfo) { + browser.test.assertEq("about:blank", changeInfo.url, + "Check changeInfo.url"); + browser.tabs.onUpdated.removeListener(onUpdated); + // Remove created tab. + browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + return; + } + }); + browser.tabs.update(tab.id, {url: "about:blank"}); + }); + }); +}); + +add_task(function* test_without_tabs_permission() { + yield do_test_update(function background() { + browser.tabs.create({url: "about:blank"}, function(tab) { + browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) { + if (tabId == tab.id) { + browser.test.assertFalse("url" in changeInfo, "url should not be included without tabs permission"); + browser.test.assertFalse("favIconUrl" in changeInfo, "favIconUrl should not be included without tabs permission"); + + if (changeInfo.status == "complete") { + browser.tabs.onUpdated.removeListener(onUpdated); + browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + } + } + }); + browser.tabs.reload(tab.id); + }); + }, false /* withPermissions */); +}); + +add_task(forceGC); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_query.js b/browser/components/extensions/test/browser/browser_ext_tabs_query.js new file mode 100644 index 000000000..7804d1454 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js @@ -0,0 +1,224 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +requestLongerTimeout(2); + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:config"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + lastFocusedWindow: true, + }, function(tabs) { + browser.test.assertEq(tabs.length, 3, "should have three tabs"); + + tabs.sort((tab1, tab2) => tab1.index - tab2.index); + + browser.test.assertEq(tabs[0].url, "about:blank", "first tab blank"); + tabs.shift(); + + browser.test.assertTrue(tabs[0].active, "tab 0 active"); + browser.test.assertFalse(tabs[1].active, "tab 1 inactive"); + + browser.test.assertFalse(tabs[0].pinned, "tab 0 unpinned"); + browser.test.assertFalse(tabs[1].pinned, "tab 1 unpinned"); + + browser.test.assertEq(tabs[0].url, "about:robots", "tab 0 url correct"); + browser.test.assertEq(tabs[1].url, "about:config", "tab 1 url correct"); + + browser.test.assertEq(tabs[0].status, "complete", "tab 0 status correct"); + browser.test.assertEq(tabs[1].status, "complete", "tab 1 status correct"); + + browser.test.assertEq(tabs[0].title, "Gort! Klaatu barada nikto!", "tab 0 title correct"); + + browser.test.notifyPass("tabs.query"); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.query"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); + + tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/"); + let tab3 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://test1.example.org/MochiKit/"); + + // test simple queries + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + url: "<all_urls>", + }, function(tabs) { + browser.test.assertEq(tabs.length, 3, "should have three tabs"); + + tabs.sort((tab1, tab2) => tab1.index - tab2.index); + + browser.test.assertEq(tabs[0].url, "http://example.com/", "tab 0 url correct"); + browser.test.assertEq(tabs[1].url, "http://example.net/", "tab 1 url correct"); + browser.test.assertEq(tabs[2].url, "http://test1.example.org/MochiKit/", "tab 2 url correct"); + + browser.test.notifyPass("tabs.query"); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.query"); + yield extension.unload(); + + // match pattern + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + url: "http://*/MochiKit*", + }, function(tabs) { + browser.test.assertEq(tabs.length, 1, "should have one tab"); + + browser.test.assertEq(tabs[0].url, "http://test1.example.org/MochiKit/", "tab 0 url correct"); + + browser.test.notifyPass("tabs.query"); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.query"); + yield extension.unload(); + + // match array of patterns + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + url: ["http://*/MochiKit*", "http://*.com/*"], + }, function(tabs) { + browser.test.assertEq(tabs.length, 2, "should have two tabs"); + + tabs.sort((tab1, tab2) => tab1.index - tab2.index); + + browser.test.assertEq(tabs[0].url, "http://example.com/", "tab 0 url correct"); + browser.test.assertEq(tabs[1].url, "http://test1.example.org/MochiKit/", "tab 1 url correct"); + + browser.test.notifyPass("tabs.query"); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.query"); + yield extension.unload(); + + // test width and height + extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.test.onMessage.addListener(async msg => { + let tabs = await browser.tabs.query({active: true}); + + browser.test.assertEq(tabs.length, 1, "should have one tab"); + browser.test.sendMessage("dims", {width: tabs[0].width, height: tabs[0].height}); + }); + browser.test.sendMessage("ready"); + }, + }); + + const RESOLUTION_PREF = "layout.css.devPixelsPerPx"; + registerCleanupFunction(() => { + SpecialPowers.clearUserPref(RESOLUTION_PREF); + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + for (let resolution of [2, 1]) { + SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution)); + is(window.devicePixelRatio, resolution, "window has the required resolution"); + + let {clientHeight, clientWidth} = gBrowser.selectedBrowser; + + extension.sendMessage("check-size"); + let dims = yield extension.awaitMessage("dims"); + is(dims.width, clientWidth, "tab reports expected width"); + is(dims.height, clientHeight, "tab reports expected height"); + } + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); + yield BrowserTestUtils.removeTab(tab3); + SpecialPowers.clearUserPref(RESOLUTION_PREF); +}); + +add_task(function* testQueryPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": [], + }, + + async background() { + try { + let tabs = await browser.tabs.query({currentWindow: true, active: true}); + browser.test.assertEq(tabs.length, 1, "Expect query to return tabs"); + browser.test.notifyPass("queryPermissions"); + } catch (e) { + browser.test.notifyFail("queryPermissions"); + } + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("queryPermissions"); + + yield extension.unload(); +}); + +add_task(function* testQueryWithURLPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": [], + }, + + async background() { + await browser.test.assertRejects( + browser.tabs.query({"url": "http://www.bbc.com/"}), + 'The "tabs" permission is required to use the query API with the "url" parameter', + "Expected tabs.query with 'url' to fail with permissions error message"); + + browser.test.notifyPass("queryWithURLPermissions"); + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("queryWithURLPermissions"); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_reload.js b/browser/components/extensions/test/browser/browser_ext_tabs_reload.js new file mode 100644 index 000000000..99b2d426b --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_reload.js @@ -0,0 +1,54 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + files: { + "tab.js": function() { + browser.runtime.sendMessage("tab-loaded"); + }, + "tab.html": + `<head> + <meta charset="utf-8"> + <script src="tab.js"></script> + </head>`, + }, + + async background() { + let tabLoadedCount = 0; + + let tab = await browser.tabs.create({url: "tab.html", active: true}); + + browser.runtime.onMessage.addListener(msg => { + if (msg == "tab-loaded") { + tabLoadedCount++; + + if (tabLoadedCount == 1) { + // Reload the tab once passing no arguments. + return browser.tabs.reload(); + } + + if (tabLoadedCount == 2) { + // Reload the tab again with explicit arguments. + return browser.tabs.reload(tab.id, { + bypassCache: false, + }); + } + + if (tabLoadedCount == 3) { + browser.test.notifyPass("tabs.reload"); + } + } + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.reload"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js b/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js new file mode 100644 index 000000000..648361724 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js @@ -0,0 +1,58 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs", "<all_urls>"], + }, + + async background() { + const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/"; + const URL = BASE + "file_bypass_cache.sjs"; + + function awaitLoad(tabId) { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) { + if (tabId == tabId_ && changed.status == "complete" && tab.url == URL) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + } + + try { + let tab = await browser.tabs.create({url: URL}); + await awaitLoad(tab.id); + + await browser.tabs.reload(tab.id, {bypassCache: false}); + await awaitLoad(tab.id); + + let [textContent] = await browser.tabs.executeScript(tab.id, {code: "document.body.textContent"}); + browser.test.assertEq("", textContent, "`textContent` should be empty when bypassCache=false"); + + await browser.tabs.reload(tab.id, {bypassCache: true}); + await awaitLoad(tab.id); + + [textContent] = await browser.tabs.executeScript(tab.id, {code: "document.body.textContent"}); + + let [pragma, cacheControl] = textContent.split(":"); + browser.test.assertEq("no-cache", pragma, "`pragma` should be set to `no-cache` when bypassCache is true"); + browser.test.assertEq("no-cache", cacheControl, "`cacheControl` should be set to `no-cache` when bypassCache is true"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("tabs.reload_bypass_cache"); + } catch (error) { + browser.test.fail(`${error} :: ${error.stack}`); + browser.test.notifyFail("tabs.reload_bypass_cache"); + } + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("tabs.reload_bypass_cache"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js new file mode 100644 index 000000000..e0eadab64 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js @@ -0,0 +1,95 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testExecuteScript() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true); + + async function background() { + let tasks = [ + // Insert CSS file. + { + background: "transparent", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.insertCSS({ + file: "file2.css", + }); + }, + }, + // Insert CSS code. + { + background: "rgb(42, 42, 42)", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.insertCSS({ + code: "* { background: rgb(42, 42, 42) }", + }); + }, + }, + // Remove CSS code again. + { + background: "transparent", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.removeCSS({ + code: "* { background: rgb(42, 42, 42) }", + }); + }, + }, + // Remove CSS file again. + { + background: "transparent", + foreground: "rgb(0, 0, 0)", + promise: () => { + return browser.tabs.removeCSS({ + file: "file2.css", + }); + }, + }, + ]; + + function checkCSS() { + let computedStyle = window.getComputedStyle(document.body); + return [computedStyle.backgroundColor, computedStyle.color]; + } + + try { + for (let {promise, background, foreground} of tasks) { + let result = await promise(); + browser.test.assertEq(undefined, result, "Expected callback result"); + + [result] = await browser.tabs.executeScript({ + code: `(${checkCSS})()`, + }); + browser.test.assertEq(background, result[0], "Expected background color"); + browser.test.assertEq(foreground, result[1], "Expected foreground color"); + } + + browser.test.notifyPass("removeCSS"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFailure("removeCSS"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/"], + }, + + background, + + files: { + "file2.css": "* { color: rgb(0, 113, 4) }", + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("removeCSS"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js new file mode 100644 index 000000000..64e97afb1 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js @@ -0,0 +1,227 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* tabsSendMessageReply() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + + "content_scripts": [{ + "matches": ["http://example.com/"], + "js": ["content-script.js"], + "run_at": "document_start", + }], + }, + + background: async function() { + let firstTab; + let promiseResponse = new Promise(resolve => { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "content-script-ready") { + let tabId = sender.tab.id; + + Promise.all([ + promiseResponse, + + browser.tabs.sendMessage(tabId, "respond-now"), + browser.tabs.sendMessage(tabId, "respond-now-2"), + new Promise(resolve => browser.tabs.sendMessage(tabId, "respond-soon", resolve)), + browser.tabs.sendMessage(tabId, "respond-promise"), + browser.tabs.sendMessage(tabId, "respond-never"), + new Promise(resolve => { + browser.runtime.sendMessage("respond-never", response => { resolve(response); }); + }), + + browser.tabs.sendMessage(tabId, "respond-error").catch(error => Promise.resolve({error})), + browser.tabs.sendMessage(tabId, "throw-error").catch(error => Promise.resolve({error})), + + browser.tabs.sendMessage(firstTab, "no-listener").catch(error => Promise.resolve({error})), + ]).then(([response, respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondNever2, respondError, throwError, noListener]) => { + browser.test.assertEq("expected-response", response, "Content script got the expected response"); + + browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response"); + browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener"); + browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response"); + browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response"); + browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution"); + browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution"); + + browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response"); + browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response"); + + browser.test.assertEq("Could not establish connection. Receiving end does not exist.", + noListener.error.message, + "Got the expected no listener response"); + + return browser.tabs.remove(tabId); + }).then(() => { + browser.test.notifyPass("sendMessage"); + }); + + return Promise.resolve("expected-response"); + } else if (msg[0] == "got-response") { + resolve(msg[1]); + } + }); + }); + + let tabs = await browser.tabs.query({currentWindow: true, active: true}); + firstTab = tabs[0].id; + browser.tabs.create({url: "http://example.com/"}); + }, + + files: { + "content-script.js": async function() { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "respond-now") { + respond(msg); + } else if (msg == "respond-soon") { + setTimeout(() => { respond(msg); }, 0); + return true; + } else if (msg == "respond-promise") { + return Promise.resolve(msg); + } else if (msg == "respond-never") { + return; + } else if (msg == "respond-error") { + return Promise.reject(new Error(msg)); + } else if (msg == "throw-error") { + throw new Error(msg); + } + }); + + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "respond-now") { + respond("hello"); + } else if (msg == "respond-now-2") { + respond(msg); + } + }); + + let response = await browser.runtime.sendMessage("content-script-ready"); + browser.runtime.sendMessage(["got-response", response]); + }, + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("sendMessage"); + + yield extension.unload(); +}); + + +add_task(function* tabsSendHidden() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + + "content_scripts": [{ + "matches": ["http://example.com/content*"], + "js": ["content-script.js"], + "run_at": "document_start", + }], + }, + + background: async function() { + let resolveContent; + browser.runtime.onMessage.addListener((msg, sender) => { + if (msg[0] == "content-ready") { + resolveContent(msg[1]); + } + }); + + let awaitContent = url => { + return new Promise(resolve => { + resolveContent = resolve; + }).then(result => { + browser.test.assertEq(url, result, "Expected content script URL"); + }); + }; + + try { + const URL1 = "http://example.com/content1.html"; + const URL2 = "http://example.com/content2.html"; + + let tab = await browser.tabs.create({url: URL1}); + await awaitContent(URL1); + + let url = await browser.tabs.sendMessage(tab.id, URL1); + browser.test.assertEq(URL1, url, "Should get response from expected content window"); + + await browser.tabs.update(tab.id, {url: URL2}); + await awaitContent(URL2); + + url = await browser.tabs.sendMessage(tab.id, URL2); + browser.test.assertEq(URL2, url, "Should get response from expected content window"); + + // Repeat once just to be sure the first message was processed by all + // listeners before we exit the test. + url = await browser.tabs.sendMessage(tab.id, URL2); + browser.test.assertEq(URL2, url, "Should get response from expected content window"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("contentscript-bfcache-window"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("contentscript-bfcache-window"); + } + }, + + files: { + "content-script.js": function() { + // Store this in a local variable to make sure we don't touch any + // properties of the possibly-hidden content window. + let href = window.location.href; + + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.assertEq(href, msg, "Should be in the expected content window"); + + return Promise.resolve(href); + }); + + browser.runtime.sendMessage(["content-ready", href]); + }, + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("contentscript-bfcache-window"); + + yield extension.unload(); +}); + + +add_task(function* tabsSendMessageNoExceptionOnNonExistentTab() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + async background() { + let url = "http://example.com/mochitest/browser/browser/components/extensions/test/browser/file_dummy.html"; + let tab = await browser.tabs.create({url}); + + try { + browser.tabs.sendMessage(tab.id, "message"); + browser.tabs.sendMessage(tab.id + 100, "message"); + } catch (e) { + browser.test.fail("no exception should be raised on tabs.sendMessage to nonexistent tabs"); + } + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("tabs.sendMessage"); + }, + }); + + yield Promise.all([ + extension.startup(), + extension.awaitFinish("tabs.sendMessage"), + ]); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update.js b/browser/components/extensions/test/browser/browser_ext_tabs_update.js new file mode 100644 index 000000000..8e56a746c --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_update.js @@ -0,0 +1,45 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:config"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + browser.tabs.query({ + lastFocusedWindow: true, + }, function(tabs) { + browser.test.assertEq(tabs.length, 3, "should have three tabs"); + + tabs.sort((tab1, tab2) => tab1.index - tab2.index); + + browser.test.assertEq(tabs[0].url, "about:blank", "first tab blank"); + tabs.shift(); + + browser.test.assertTrue(tabs[0].active, "tab 0 active"); + browser.test.assertFalse(tabs[1].active, "tab 1 inactive"); + + browser.tabs.update(tabs[1].id, {active: true}, function() { + browser.test.sendMessage("check"); + }); + }); + }, + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("check")]); + + ok(gBrowser.selectedTab == tab2, "correct tab selected"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js new file mode 100644 index 000000000..b43855fb1 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js @@ -0,0 +1,110 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* testTabsUpdateURL(existentTabURL, tabsUpdateURL, isErrorExpected) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + files: { + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>tab page</h1> + </body> + </html> + `.trim(), + }, + background: function() { + browser.test.sendMessage("ready", browser.runtime.getURL("tab.html")); + + browser.test.onMessage.addListener(async (msg, tabsUpdateURL, isErrorExpected) => { + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + + try { + let tab = await browser.tabs.update(tabs[1].id, {url: tabsUpdateURL}); + + browser.test.assertFalse(isErrorExpected, `tabs.update with URL ${tabsUpdateURL} should be rejected`); + browser.test.assertTrue(tab, "on success the tab should be defined"); + } catch (error) { + browser.test.assertTrue(isErrorExpected, `tabs.update with URL ${tabsUpdateURL} should not be rejected`); + browser.test.assertTrue(/^Illegal URL/.test(error.message), + "tabs.update should be rejected with the expected error message"); + } + + browser.test.sendMessage("done"); + }); + }, + }); + + yield extension.startup(); + + let mozExtTabURL = yield extension.awaitMessage("ready"); + + if (tabsUpdateURL == "self") { + tabsUpdateURL = mozExtTabURL; + } + + info(`tab.update URL "${tabsUpdateURL}" on tab with URL "${existentTabURL}"`); + + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, existentTabURL); + + extension.sendMessage("start", tabsUpdateURL, isErrorExpected); + yield extension.awaitMessage("done"); + + yield BrowserTestUtils.removeTab(tab1); + yield extension.unload(); +} + +add_task(function* () { + info("Start testing tabs.update on javascript URLs"); + + let dataURLPage = `data:text/html, + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>data url page</h1> + </body> + </html>`; + + let checkList = [ + { + tabsUpdateURL: "http://example.net", + isErrorExpected: false, + }, + { + tabsUpdateURL: "self", + isErrorExpected: false, + }, + { + tabsUpdateURL: "about:addons", + isErrorExpected: true, + }, + { + tabsUpdateURL: "javascript:console.log('tabs.update execute javascript')", + isErrorExpected: true, + }, + { + tabsUpdateURL: dataURLPage, + isErrorExpected: true, + }, + ]; + + let testCases = checkList + .map((check) => Object.assign({}, check, {existentTabURL: "about:blank"})); + + for (let {existentTabURL, tabsUpdateURL, isErrorExpected} of testCases) { + yield* testTabsUpdateURL(existentTabURL, tabsUpdateURL, isErrorExpected); + } + + info("done"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js new file mode 100644 index 000000000..c2e54d3ea --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js @@ -0,0 +1,222 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const SITE_SPECIFIC_PREF = "browser.zoom.siteSpecific"; + +add_task(function* () { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/"); + + gBrowser.selectedTab = tab1; + + async function background() { + function promiseUpdated(tabId, attr) { + return new Promise(resolve => { + let onUpdated = (tabId_, changeInfo, tab) => { + if (tabId == tabId_ && attr in changeInfo) { + browser.tabs.onUpdated.removeListener(onUpdated); + + resolve({changeInfo, tab}); + } + }; + browser.tabs.onUpdated.addListener(onUpdated); + }); + } + + let deferred = {}; + browser.test.onMessage.addListener((message, msg, result) => { + if (message == "msg-done" && deferred[msg]) { + deferred[msg].resolve(result); + } + }); + + let _id = 0; + function msg(...args) { + return new Promise((resolve, reject) => { + let id = ++_id; + deferred[id] = {resolve, reject}; + browser.test.sendMessage("msg", id, ...args); + }); + } + + + let zoomEvents = []; + let eventPromises = []; + browser.tabs.onZoomChange.addListener(info => { + zoomEvents.push(info); + if (eventPromises.length) { + eventPromises.shift().resolve(); + } + }); + + let awaitZoom = async (tabId, newValue) => { + let listener; + + await new Promise(async resolve => { + listener = info => { + if (info.tabId == tabId && info.newZoomFactor == newValue) { + resolve(); + } + }; + browser.tabs.onZoomChange.addListener(listener); + + let zoomFactor = await browser.tabs.getZoom(tabId); + if (zoomFactor == newValue) { + resolve(); + } + }); + + browser.tabs.onZoomChange.removeListener(listener); + }; + + let checkZoom = async (tabId, newValue, oldValue = null) => { + let awaitEvent; + if (oldValue != null && !zoomEvents.length) { + awaitEvent = new Promise(resolve => { + eventPromises.push({resolve}); + }); + } + + let [apiZoom, realZoom] = await Promise.all([ + browser.tabs.getZoom(tabId), + msg("get-zoom", tabId), + awaitEvent, + ]); + + browser.test.assertEq(newValue, apiZoom, `Got expected zoom value from API`); + browser.test.assertEq(newValue, realZoom, `Got expected zoom value from parent`); + + if (oldValue != null) { + let event = zoomEvents.shift(); + browser.test.assertEq(tabId, event.tabId, `Got expected zoom event tab ID`); + browser.test.assertEq(newValue, event.newZoomFactor, `Got expected zoom event zoom factor`); + browser.test.assertEq(oldValue, event.oldZoomFactor, `Got expected zoom event old zoom factor`); + + browser.test.assertEq(3, Object.keys(event.zoomSettings).length, `Zoom settings should have 3 keys`); + browser.test.assertEq("automatic", event.zoomSettings.mode, `Mode should be "automatic"`); + browser.test.assertEq("per-origin", event.zoomSettings.scope, `Scope should be "per-origin"`); + browser.test.assertEq(1, event.zoomSettings.defaultZoomFactor, `Default zoom should be 1`); + } + }; + + try { + let tabs = await browser.tabs.query({lastFocusedWindow: true}); + browser.test.assertEq(tabs.length, 3, "We have three tabs"); + + let tabIds = [tabs[1].id, tabs[2].id]; + await checkZoom(tabIds[0], 1); + + await browser.tabs.setZoom(tabIds[0], 2); + await checkZoom(tabIds[0], 2, 1); + + let zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]); + browser.test.assertEq(3, Object.keys(zoomSettings).length, `Zoom settings should have 3 keys`); + browser.test.assertEq("automatic", zoomSettings.mode, `Mode should be "automatic"`); + browser.test.assertEq("per-origin", zoomSettings.scope, `Scope should be "per-origin"`); + browser.test.assertEq(1, zoomSettings.defaultZoomFactor, `Default zoom should be 1`); + + + browser.test.log(`Switch to tab 2`); + await browser.tabs.update(tabIds[1], {active: true}); + await checkZoom(tabIds[1], 1); + + + browser.test.log(`Navigate tab 2 to origin of tab 1`); + browser.tabs.update(tabIds[1], {url: "http://example.com"}); + await promiseUpdated(tabIds[1], "url"); + await checkZoom(tabIds[1], 2, 1); + + + browser.test.log(`Update zoom in tab 2, expect changes in both tabs`); + await browser.tabs.setZoom(tabIds[1], 1.5); + await checkZoom(tabIds[1], 1.5, 2); + + + browser.test.log(`Switch to tab 1, expect asynchronous zoom change just after the switch`); + await Promise.all([ + awaitZoom(tabIds[0], 1.5), + browser.tabs.update(tabIds[0], {active: true}), + ]); + await checkZoom(tabIds[0], 1.5, 2); + + + browser.test.log("Set zoom to 0, expect it set to 1"); + await browser.tabs.setZoom(tabIds[0], 0); + await checkZoom(tabIds[0], 1, 1.5); + + + browser.test.log("Change zoom externally, expect changes reflected"); + await msg("enlarge"); + await checkZoom(tabIds[0], 1.1, 1); + + await Promise.all([ + browser.tabs.setZoom(tabIds[0], 0), + browser.tabs.setZoom(tabIds[1], 0), + ]); + await Promise.all([ + checkZoom(tabIds[0], 1, 1.1), + checkZoom(tabIds[1], 1, 1.5), + ]); + + + browser.test.log("Check that invalid zoom values throw an error"); + await browser.test.assertRejects( + browser.tabs.setZoom(tabIds[0], 42), + /Zoom value 42 out of range/, + "Expected an out of range error"); + + browser.test.log("Disable site-specific zoom, expect correct scope"); + await msg("site-specific", false); + zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]); + + browser.test.assertEq("per-tab", zoomSettings.scope, `Scope should be "per-tab"`); + await msg("site-specific", null); + + browser.test.notifyPass("tab-zoom"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("tab-zoom"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + extension.onMessage("msg", (id, msg, ...args) => { + let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let resp; + if (msg == "get-zoom") { + let tab = TabManager.getTab(args[0]); + resp = ZoomManager.getZoomForBrowser(tab.linkedBrowser); + } else if (msg == "set-zoom") { + let tab = TabManager.getTab(args[0]); + ZoomManager.setZoomForBrowser(tab.linkedBrowser); + } else if (msg == "enlarge") { + FullZoom.enlarge(); + } else if (msg == "site-specific") { + if (args[0] == null) { + SpecialPowers.clearUserPref(SITE_SPECIFIC_PREF); + } else { + SpecialPowers.setBoolPref(SITE_SPECIFIC_PREF, args[0]); + } + } + + extension.sendMessage("msg-done", id, resp); + }); + + yield extension.startup(); + + yield extension.awaitFinish("tab-zoom"); + + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_topwindowid.js b/browser/components/extensions/test/browser/browser_ext_topwindowid.js new file mode 100644 index 000000000..9176ac946 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_topwindowid.js @@ -0,0 +1,23 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_topwindowid_cleanup() { + let {Frames} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + let {outerWindowID, messageManager} = tab.linkedBrowser; + + ok(Frames.topWindowIds.has(outerWindowID), "Outer window ID is registered"); + + let awaitDisconnect = TestUtils.topicObserved("message-manager-disconnect", + subject => subject === messageManager); + + yield BrowserTestUtils.removeTab(tab); + + yield awaitDisconnect; + + ok(!Frames.topWindowIds.has(outerWindowID), "Outer window ID is no longer registered"); +}); + diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js new file mode 100644 index 000000000..0058ca065 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js @@ -0,0 +1,45 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* webNavigation_getFrameId_of_existing_main_frame() { + // Whether the frame ID in the extension API is 0 is determined by a map that + // is maintained by |Frames| in ExtensionManagement.jsm. This map is filled + // using data from content processes. But if ExtensionManagement.jsm is not + // imported, then the "Extension:TopWindowID" message gets lost. + // As a result, if the state is not synchronized again, the webNavigation API + // will mistakenly report a non-zero frame ID for top-level frames. + // + // If you want to be absolutely sure that the frame ID is correct, don't open + // tabs before starting an extension, or explicitly load the module in the + // main process: + // Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); + // + // Or simply run the test again. + const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/"; + const DUMMY_URL = BASE + "file_dummy.html"; + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_URL, true); + + async function background(DUMMY_URL) { + let tabs = await browser.tabs.query({active: true, currentWindow: true}); + let frames = await browser.webNavigation.getAllFrames({tabId: tabs[0].id}); + browser.test.assertEq(1, frames.length, "The dummy page has one frame"); + browser.test.assertEq(0, frames[0].frameId, "Main frame's ID must be 0"); + browser.test.assertEq(DUMMY_URL, frames[0].url, "Main frame URL must match"); + browser.test.notifyPass("frameId checked"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["webNavigation"], + }, + + background: `(${background})(${JSON.stringify(DUMMY_URL)});`, + }); + + yield extension.startup(); + yield extension.awaitFinish("frameId checked"); + yield extension.unload(); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js new file mode 100644 index 000000000..6b4a597ad --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js @@ -0,0 +1,168 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testWebNavigationGetNonExistentTab() { + let extension = ExtensionTestUtils.loadExtension({ + background: async function() { + // There is no "tabId = 0" because the id assigned by TabManager (defined in ext-utils.js) + // starts from 1. + await browser.test.assertRejects( + browser.webNavigation.getAllFrames({tabId: 0}), + "Invalid tab ID: 0", + "getAllFrames rejected Promise should pass the expected error"); + + // There is no "tabId = 0" because the id assigned by TabManager (defined in ext-utils.js) + // starts from 1, processId is currently marked as optional and it is ignored. + await browser.test.assertRejects( + browser.webNavigation.getFrame({tabId: 0, frameId: 15, processId: 20}), + "Invalid tab ID: 0", + "getFrame rejected Promise should pass the expected error"); + + browser.test.sendMessage("getNonExistentTab.done"); + }, + manifest: { + permissions: ["webNavigation"], + }, + }); + info("load complete"); + + yield extension.startup(); + info("startup complete"); + + yield extension.awaitMessage("getNonExistentTab.done"); + + yield extension.unload(); + info("extension unloaded"); +}); + +add_task(function* testWebNavigationFrames() { + let extension = ExtensionTestUtils.loadExtension({ + background: async function() { + let tabId; + let collectedDetails = []; + + browser.webNavigation.onCompleted.addListener(async details => { + collectedDetails.push(details); + + if (details.frameId !== 0) { + // wait for the top level iframe to be complete + return; + } + + let getAllFramesDetails = await browser.webNavigation.getAllFrames({tabId}); + + let getFramePromises = getAllFramesDetails.map(({frameId}) => { + // processId is currently marked as optional and it is ignored. + return browser.webNavigation.getFrame({tabId, frameId, processId: 0}); + }); + + let getFrameResults = await Promise.all(getFramePromises); + browser.test.sendMessage("webNavigationFrames.done", { + collectedDetails, getAllFramesDetails, getFrameResults, + }); + + // Pick a random frameId. + let nonExistentFrameId = Math.floor(Math.random() * 10000); + + // Increment the picked random nonExistentFrameId until it doesn't exists. + while (getAllFramesDetails.filter((details) => details.frameId == nonExistentFrameId).length > 0) { + nonExistentFrameId += 1; + } + + // Check that getFrame Promise is rejected with the expected error message on nonexistent frameId. + await browser.test.assertRejects( + browser.webNavigation.getFrame({tabId, frameId: nonExistentFrameId, processId: 20}), + `No frame found with frameId: ${nonExistentFrameId}`, + "getFrame promise should be rejected with the expected error message on unexistent frameId"); + + await browser.tabs.remove(tabId); + browser.test.sendMessage("webNavigationFrames.done"); + }); + + let tab = await browser.tabs.create({url: "tab.html"}); + tabId = tab.id; + }, + manifest: { + permissions: ["webNavigation", "tabs"], + }, + files: { + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="subframe.html"></iframe> + <iframe src="subframe.html"></iframe> + </body> + </html> + `, + "subframe.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + </html> + `, + }, + }); + info("load complete"); + + yield extension.startup(); + info("startup complete"); + + let { + collectedDetails, + getAllFramesDetails, + getFrameResults, + } = yield extension.awaitMessage("webNavigationFrames.done"); + + is(getAllFramesDetails.length, 3, "expected number of frames found"); + is(getAllFramesDetails.length, collectedDetails.length, + "number of frames found should equal the number onCompleted events collected"); + + is(getAllFramesDetails[0].frameId, 0, "the root frame has the expected frameId"); + is(getAllFramesDetails[0].parentFrameId, -1, "the root frame has the expected parentFrameId"); + + // ordered by frameId + let sortByFrameId = (el1, el2) => { + let val1 = el1 ? el1.frameId : -1; + let val2 = el2 ? el2.frameId : -1; + return val1 - val2; + }; + + collectedDetails = collectedDetails.sort(sortByFrameId); + getAllFramesDetails = getAllFramesDetails.sort(sortByFrameId); + getFrameResults = getFrameResults.sort(sortByFrameId); + + info("check frame details content"); + + is(getFrameResults.length, getAllFramesDetails.length, + "getFrame and getAllFrames should return the same number of results"); + + Assert.deepEqual(getFrameResults, getAllFramesDetails, + "getFrame and getAllFrames should return the same results"); + + info(`check frame details collected and retrieved with getAllFrames`); + + for (let [i, collected] of collectedDetails.entries()) { + let getAllFramesDetail = getAllFramesDetails[i]; + + is(getAllFramesDetail.frameId, collected.frameId, "frameId"); + is(getAllFramesDetail.parentFrameId, collected.parentFrameId, "parentFrameId"); + is(getAllFramesDetail.tabId, collected.tabId, "tabId"); + + // This can be uncommented once Bug 1246125 has been fixed + // is(getAllFramesDetail.url, collected.url, "url"); + } + + info("frame details content checked"); + + yield extension.awaitMessage("webNavigationFrames.done"); + + yield extension.unload(); + info("extension unloaded"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js new file mode 100644 index 000000000..f2ea0d901 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js @@ -0,0 +1,251 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); + +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +function* promiseAutocompleteResultPopup(inputText) { + gURLBar.focus(); + gURLBar.value = inputText; + gURLBar.controller.startSearch(inputText); + yield promisePopupShown(gURLBar.popup); + yield BrowserTestUtils.waitForCondition(() => { + return gURLBar.controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH; + }); +} + +function* addBookmark(bookmark) { + if (bookmark.keyword) { + yield PlacesUtils.keywords.insert({ + keyword: bookmark.keyword, + url: bookmark.url, + }); + } + + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: bookmark.url, + title: bookmark.title, + }); + + registerCleanupFunction(function* () { + yield PlacesUtils.bookmarks.eraseEverything(); + }); +} + +function addSearchEngine(basename) { + return new Promise((resolve, reject) => { + info("Waiting for engine to be added: " + basename); + let url = getRootDirectory(gTestPath) + basename; + Services.search.addEngine(url, null, "", false, { + onSuccess: (engine) => { + info(`Search engine added: ${basename}`); + registerCleanupFunction(() => Services.search.removeEngine(engine)); + resolve(engine); + }, + onError: (errCode) => { + ok(false, `addEngine failed with error code ${errCode}`); + reject(); + }, + }); + }); +} + +function* prepareSearchEngine() { + let oldCurrentEngine = Services.search.currentEngine; + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + let engine = yield addSearchEngine(TEST_ENGINE_BASENAME); + Services.search.currentEngine = engine; + + registerCleanupFunction(function* () { + Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF); + Services.search.currentEngine = oldCurrentEngine; + + // Make sure the popup is closed for the next test. + gURLBar.blur(); + gURLBar.popup.selectedIndex = -1; + gURLBar.popup.hidePopup(); + ok(!gURLBar.popup.popupOpen, "popup should be closed"); + + // Clicking suggestions causes visits to search results pages, so clear that + // history now. + yield PlacesTestUtils.clearHistory(); + }); +} + +add_task(function* test_webnavigation_urlbar_typed_transitions() { + function backgroundScript() { + browser.webNavigation.onCommitted.addListener((msg) => { + browser.test.assertEq("http://example.com/?q=typed", msg.url, + "Got the expected url"); + // assert from_address_bar transition qualifier + browser.test.assertTrue(msg.transitionQualifiers && + msg.transitionQualifiers.includes("from_address_bar"), + "Got the expected from_address_bar transitionQualifier"); + browser.test.assertEq("typed", msg.transitionType, + "Got the expected transitionType"); + browser.test.notifyPass("webNavigation.from_address_bar.typed"); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["webNavigation"], + }, + }); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + gURLBar.focus(); + gURLBar.textValue = "http://example.com/?q=typed"; + + EventUtils.synthesizeKey("VK_RETURN", {altKey: true}); + + yield extension.awaitFinish("webNavigation.from_address_bar.typed"); + + yield extension.unload(); + info("extension unloaded"); +}); + +add_task(function* test_webnavigation_urlbar_bookmark_transitions() { + function backgroundScript() { + browser.webNavigation.onCommitted.addListener((msg) => { + browser.test.assertEq("http://example.com/?q=bookmark", msg.url, + "Got the expected url"); + + // assert from_address_bar transition qualifier + browser.test.assertTrue(msg.transitionQualifiers && + msg.transitionQualifiers.includes("from_address_bar"), + "Got the expected from_address_bar transitionQualifier"); + browser.test.assertEq("auto_bookmark", msg.transitionType, + "Got the expected transitionType"); + browser.test.notifyPass("webNavigation.from_address_bar.auto_bookmark"); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["webNavigation"], + }, + }); + + yield addBookmark({ + title: "Bookmark To Click", + url: "http://example.com/?q=bookmark", + }); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + yield promiseAutocompleteResultPopup("Bookmark To Click"); + + let item = gURLBar.popup.richlistbox.getItemAtIndex(1); + item.click(); + yield extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark"); + + yield extension.unload(); + info("extension unloaded"); +}); + +add_task(function* test_webnavigation_urlbar_keyword_transition() { + function backgroundScript() { + browser.webNavigation.onCommitted.addListener((msg) => { + browser.test.assertEq(`http://example.com/?q=search`, msg.url, + "Got the expected url"); + + // assert from_address_bar transition qualifier + browser.test.assertTrue(msg.transitionQualifiers && + msg.transitionQualifiers.includes("from_address_bar"), + "Got the expected from_address_bar transitionQualifier"); + browser.test.assertEq("keyword", msg.transitionType, + "Got the expected transitionType"); + browser.test.notifyPass("webNavigation.from_address_bar.keyword"); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["webNavigation"], + }, + }); + + yield addBookmark({ + title: "Test Keyword", + url: "http://example.com/?q=%s", + keyword: "testkw", + }); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + yield promiseAutocompleteResultPopup("testkw search"); + + let item = gURLBar.popup.richlistbox.getItemAtIndex(0); + item.click(); + + yield extension.awaitFinish("webNavigation.from_address_bar.keyword"); + + yield extension.unload(); + info("extension unloaded"); +}); + +add_task(function* test_webnavigation_urlbar_search_transitions() { + function backgroundScript() { + browser.webNavigation.onCommitted.addListener((msg) => { + browser.test.assertEq("http://mochi.test:8888/", msg.url, + "Got the expected url"); + + // assert from_address_bar transition qualifier + browser.test.assertTrue(msg.transitionQualifiers && + msg.transitionQualifiers.includes("from_address_bar"), + "Got the expected from_address_bar transitionQualifier"); + browser.test.assertEq("generated", msg.transitionType, + "Got the expected 'generated' transitionType"); + browser.test.notifyPass("webNavigation.from_address_bar.generated"); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["webNavigation"], + }, + }); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + yield prepareSearchEngine(); + yield promiseAutocompleteResultPopup("foo"); + + let item = gURLBar.popup.richlistbox.getItemAtIndex(0); + item.click(); + + yield extension.awaitFinish("webNavigation.from_address_bar.generated"); + + yield extension.unload(); + info("extension unloaded"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_webRequest.js b/browser/components/extensions/test/browser/browser_ext_webRequest.js new file mode 100644 index 000000000..ab9f58480 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_webRequest.js @@ -0,0 +1,95 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* globals makeExtension */ +"use strict"; + +Services.scriptloader.loadSubScript(new URL("head_webrequest.js", gTestPath).href, + this); + +Cu.import("resource:///modules/HiddenFrame.jsm", this); +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +function createHiddenBrowser(url) { + let frame = new HiddenFrame(); + return new Promise(resolve => + frame.get().then(subframe => { + let doc = subframe.document; + let browser = doc.createElementNS(XUL_NS, "browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("src", url); + + doc.documentElement.appendChild(browser); + resolve({frame: frame, browser: browser}); + })); +} + +let extension; +let dummy = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_dummy.html"; + +add_task(function* setup() { + // SelfSupport has a tendency to fire when running this test alone, without + // a good way to turn it off we just set the url to "" + yield SpecialPowers.pushPrefEnv({ + set: [["browser.selfsupport.url", ""]], + }); + extension = makeExtension(); + yield extension.startup(); +}); + +add_task(function* test_newWindow() { + let expect = { + "file_dummy.html": { + type: "main_frame", + }, + }; + // NOTE: When running solo, favicon will be loaded at some point during + // the tests in this file, so all tests ignore it. When running with + // other tests in this directory, favicon gets loaded at some point before + // we run, and we never see the request, thus it cannot be handled as part + // of expect above. + extension.sendMessage("set-expected", {expect, ignore: ["favicon.ico"]}); + yield extension.awaitMessage("continue"); + + let openedWindow = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.openNewForegroundTab(openedWindow.gBrowser, dummy + "?newWindow"); + + yield extension.awaitMessage("done"); + yield BrowserTestUtils.closeWindow(openedWindow); +}); + +add_task(function* test_newTab() { + // again, in this window + let expect = { + "file_dummy.html": { + type: "main_frame", + }, + }; + extension.sendMessage("set-expected", {expect, ignore: ["favicon.ico"]}); + yield extension.awaitMessage("continue"); + let tab = yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, dummy + "?newTab"); + + yield extension.awaitMessage("done"); + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_subframe() { + let expect = { + "file_dummy.html": { + type: "main_frame", + }, + }; + // test a content subframe attached to hidden window + extension.sendMessage("set-expected", {expect, ignore: ["favicon.ico"]}); + yield extension.awaitMessage("continue"); + let frameInfo = yield createHiddenBrowser(dummy + "?subframe"); + yield extension.awaitMessage("done"); + // cleanup + frameInfo.browser.remove(); + frameInfo.frame.destroy(); +}); + +add_task(function* teardown() { + yield extension.unload(); +}); + diff --git a/browser/components/extensions/test/browser/browser_ext_windows.js b/browser/components/extensions/test/browser/browser_ext_windows.js new file mode 100644 index 000000000..d3dd6ecdb --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows.js @@ -0,0 +1,33 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + let raisedWin = Services.ww.openWindow( + null, Services.prefs.getCharPref("browser.chromeURL"), "_blank", + "chrome,dialog=no,all,alwaysRaised", null); + + yield TestUtils.topicObserved("browser-delayed-startup-finished", + subject => subject == raisedWin); + + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.windows.getAll((wins) => { + browser.test.assertEq(wins.length, 2, "Expect two windows"); + + browser.test.assertEq(false, wins[0].alwaysOnTop, + "Expect first window not to be always on top"); + browser.test.assertEq(true, wins[1].alwaysOnTop, + "Expect first window to be always on top"); + + browser.test.notifyPass("alwaysOnTop"); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alwaysOnTop"); + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(raisedWin); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js b/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js new file mode 100644 index 000000000..13f8b2eee --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js @@ -0,0 +1,61 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Tests allowScriptsToClose option +add_task(function* test_allowScriptsToClose() { + const files = { + "dummy.html": "<meta charset=utf-8><script src=close.js></script>", + "close.js": function() { + window.close(); + if (!window.closed) { + browser.test.sendMessage("close-failed"); + } + }, + }; + + function background() { + browser.test.onMessage.addListener((msg, options) => { + function listener(_, {status}, {url}) { + if (status == "complete" && url == options.url) { + browser.tabs.onUpdated.removeListener(listener); + browser.tabs.executeScript({file: "close.js"}); + } + } + options.url = browser.runtime.getURL(options.url); + browser.windows.create(options); + if (msg === "create+execute") { + browser.tabs.onUpdated.addListener(listener); + } + }); + browser.test.notifyPass(); + } + + const example = "http://example.com/"; + const manifest = {permissions: ["tabs", example]}; + + const extension = ExtensionTestUtils.loadExtension({files, background, manifest}); + yield SpecialPowers.pushPrefEnv({set: [["dom.allow_scripts_to_close_windows", false]]}); + + yield extension.startup(); + yield extension.awaitFinish(); + + extension.sendMessage("create", {url: "dummy.html"}); + let win = yield BrowserTestUtils.waitForNewWindow(); + yield BrowserTestUtils.windowClosed(win); + info("script allowed to close the window"); + + extension.sendMessage("create+execute", {url: example}); + win = yield BrowserTestUtils.waitForNewWindow(); + yield extension.awaitMessage("close-failed"); + info("script prevented from closing the window"); + win.close(); + + extension.sendMessage("create+execute", {url: example, allowScriptsToClose: true}); + win = yield BrowserTestUtils.waitForNewWindow(); + yield BrowserTestUtils.windowClosed(win); + info("script allowed to close the window"); + + yield SpecialPowers.popPrefEnv(); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create.js b/browser/components/extensions/test/browser/browser_ext_windows_create.js new file mode 100644 index 000000000..f209c9836 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_create.js @@ -0,0 +1,142 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +requestLongerTimeout(2); + +add_task(function* testWindowCreate() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + let _checkWindowPromise; + browser.test.onMessage.addListener(msg => { + if (msg == "checked-window") { + _checkWindowPromise.resolve(); + _checkWindowPromise = null; + } + }); + + let os; + + function checkWindow(expected) { + return new Promise(resolve => { + _checkWindowPromise = {resolve}; + browser.test.sendMessage("check-window", expected); + }); + } + + async function createWindow(params, expected, keep = false) { + let window = await browser.windows.create(...params); + // params is null when testing create without createData + params = params[0] || {}; + + for (let key of Object.keys(params)) { + if (key == "state" && os == "mac" && params.state == "normal") { + // OS-X doesn't have a hard distinction between "normal" and + // "maximized" states. + browser.test.assertTrue(window.state == "normal" || window.state == "maximized", + `Expected window.state (currently ${window.state}) to be "normal" but will accept "maximized"`); + } else { + browser.test.assertEq(params[key], window[key], `Got expected value for window.${key}`); + } + } + + browser.test.assertEq(1, window.tabs.length, "tabs property got populated"); + + await checkWindow(expected); + if (keep) { + return window; + } + + if (params.state == "fullscreen" && os == "win") { + // FIXME: Closing a fullscreen window causes a window leak in + // Windows tests. + await browser.windows.update(window.id, {state: "normal"}); + } + await browser.windows.remove(window.id); + } + + try { + ({os} = await browser.runtime.getPlatformInfo()); + + // Set the current window to state: "normal" because the test is failing on Windows + // where the current window is maximized. + let currentWindow = await browser.windows.getCurrent(); + await browser.windows.update(currentWindow.id, {state: "normal"}); + + await createWindow([], {state: "STATE_NORMAL"}); + await createWindow([{state: "maximized"}], {state: "STATE_MAXIMIZED"}); + await createWindow([{state: "minimized"}], {state: "STATE_MINIMIZED"}); + await createWindow([{state: "normal"}], {state: "STATE_NORMAL", hiddenChrome: []}); + await createWindow([{state: "fullscreen"}], {state: "STATE_FULLSCREEN"}); + + let window = await createWindow( + [{type: "popup"}], + {hiddenChrome: ["menubar", "toolbar", "location", "directories", "status", "extrachrome"], + chromeFlags: ["CHROME_OPENAS_DIALOG"]}, + true); + + let tabs = await browser.tabs.query({windowType: "popup", active: true}); + + browser.test.assertEq(1, tabs.length, "Expected only one popup"); + browser.test.assertEq(window.id, tabs[0].windowId, "Expected new window to be returned in query"); + + await browser.windows.remove(window.id); + + browser.test.notifyPass("window-create"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-create"); + } + }, + }); + + let latestWindow; + let windowListener = (window, topic) => { + if (topic == "domwindowopened") { + latestWindow = window; + } + }; + Services.ww.registerNotification(windowListener); + + extension.onMessage("check-window", expected => { + if (expected.state != null) { + let {windowState} = latestWindow; + if (latestWindow.fullScreen) { + windowState = latestWindow.STATE_FULLSCREEN; + } + + if (expected.state == "STATE_NORMAL" && AppConstants.platform == "macosx") { + ok(windowState == window.STATE_NORMAL || windowState == window.STATE_MAXIMIZED, + `Expected windowState (currently ${windowState}) to be STATE_NORMAL but will accept STATE_MAXIMIZED`); + } else { + is(windowState, window[expected.state], + `Expected window state to be ${expected.state}`); + } + } + if (expected.hiddenChrome) { + let chromeHidden = latestWindow.document.documentElement.getAttribute("chromehidden"); + is(chromeHidden.trim().split(/\s+/).sort().join(" "), + expected.hiddenChrome.sort().join(" "), + "Got expected hidden chrome"); + } + if (expected.chromeFlags) { + let {chromeFlags} = latestWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow); + for (let flag of expected.chromeFlags) { + ok(chromeFlags & Ci.nsIWebBrowserChrome[flag], + `Expected window to have the ${flag} flag`); + } + } + + extension.sendMessage("checked-window"); + }); + + yield extension.startup(); + yield extension.awaitFinish("window-create"); + yield extension.unload(); + + Services.ww.unregisterNotification(windowListener); + latestWindow = null; +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_params.js b/browser/components/extensions/test/browser/browser_ext_windows_create_params.js new file mode 100644 index 000000000..c54d94e05 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_create_params.js @@ -0,0 +1,33 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + + +// Tests that incompatible parameters can't be used together. +add_task(function* testWindowCreateParams() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + try { + for (let state of ["minimized", "maximized", "fullscreen"]) { + for (let param of ["left", "top", "width", "height"]) { + let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`; + + await browser.test.assertRejects( + browser.windows.create({state, [param]: 100}), + RegExp(expected), + `Got expected error from create(${param}=100)`); + } + } + + browser.test.notifyPass("window-create-params"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-create-params"); + } + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("window-create-params"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js b/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js new file mode 100644 index 000000000..52ffaea8b --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js @@ -0,0 +1,140 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testWindowCreate() { + async function background() { + let promiseTabAttached = () => { + return new Promise(resolve => { + browser.tabs.onAttached.addListener(function listener() { + browser.tabs.onAttached.removeListener(listener); + resolve(); + }); + }); + }; + + let promiseTabUpdated = (expected) => { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId, changeInfo, tab) { + if (changeInfo.url === expected) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + }; + + try { + let window = await browser.windows.getCurrent(); + let windowId = window.id; + + browser.test.log("Create additional tab in window 1"); + let tab = await browser.tabs.create({windowId, url: "about:blank"}); + let tabId = tab.id; + + browser.test.log("Create a new window, adopting the new tab"); + + // Note that we want to check against actual boolean values for + // all of the `incognito` property tests. + browser.test.assertEq(false, tab.incognito, "Tab is not private"); + + { + let [, window] = await Promise.all([ + promiseTabAttached(), + browser.windows.create({tabId: tabId}), + ]); + browser.test.assertEq(false, window.incognito, "New window is not private"); + browser.test.assertEq(tabId, window.tabs[0].id, "tabs property populated correctly"); + + browser.test.log("Close the new window"); + await browser.windows.remove(window.id); + } + + { + browser.test.log("Create a new private window"); + let privateWindow = await browser.windows.create({incognito: true}); + browser.test.assertEq(true, privateWindow.incognito, "Private window is private"); + + browser.test.log("Create additional tab in private window"); + let privateTab = await browser.tabs.create({windowId: privateWindow.id}); + browser.test.assertEq(true, privateTab.incognito, "Private tab is private"); + + browser.test.log("Create a new window, adopting the new private tab"); + let [, newWindow] = await Promise.all([ + promiseTabAttached(), + browser.windows.create({tabId: privateTab.id}), + ]); + browser.test.assertEq(true, newWindow.incognito, "New private window is private"); + + browser.test.log("Close the new private window"); + await browser.windows.remove(newWindow.id); + + browser.test.log("Close the private window"); + await browser.windows.remove(privateWindow.id); + } + + + browser.test.log("Try to create a window with both a tab and a URL"); + [tab] = await browser.tabs.query({windowId, active: true}); + await browser.test.assertRejects( + browser.windows.create({tabId: tab.id, url: "http://example.com/"}), + /`tabId` may not be used in conjunction with `url`/, + "Create call failed as expected"); + + browser.test.log("Try to create a window with both a tab and an invalid incognito setting"); + await browser.test.assertRejects( + browser.windows.create({tabId: tab.id, incognito: true}), + /`incognito` property must match the incognito state of tab/, + "Create call failed as expected"); + + + browser.test.log("Try to create a window with an invalid tabId"); + await browser.test.assertRejects( + browser.windows.create({tabId: 0}), + /Invalid tab ID: 0/, + "Create call failed as expected"); + + + browser.test.log("Try to create a window with two URLs"); + let readyPromise = Promise.all([ + // tabs.onUpdated can be invoked between the call of windows.create and + // the invocation of its callback/promise, so set up the listeners + // before creating the window. + promiseTabUpdated("http://example.com/"), + promiseTabUpdated("http://example.org/"), + ]); + + window = await browser.windows.create({url: ["http://example.com/", "http://example.org/"]}); + await readyPromise; + + browser.test.assertEq(2, window.tabs.length, "2 tabs were opened in new window"); + browser.test.assertEq("about:blank", window.tabs[0].url, "about:blank, page not loaded yet"); + browser.test.assertEq("about:blank", window.tabs[1].url, "about:blank, page not loaded yet"); + + window = await browser.windows.get(window.id, {populate: true}); + + browser.test.assertEq(2, window.tabs.length, "2 tabs were opened in new window"); + browser.test.assertEq("http://example.com/", window.tabs[0].url, "Correct URL was loaded in tab 1"); + browser.test.assertEq("http://example.org/", window.tabs[1].url, "Correct URL was loaded in tab 2"); + + await browser.windows.remove(window.id); + + browser.test.notifyPass("window-create"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-create"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + yield extension.startup(); + yield extension.awaitFinish("window-create"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_url.js b/browser/components/extensions/test/browser/browser_ext_windows_create_url.js new file mode 100644 index 000000000..c5c7aaf20 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_create_url.js @@ -0,0 +1,84 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testWindowCreate() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + + background: async function() { + const EXTENSION_URL = browser.runtime.getURL("test.html"); + const REMOTE_URL = browser.runtime.getURL("test.html"); + + let windows = new class extends Map { // eslint-disable-line new-parens + get(id) { + if (!this.has(id)) { + let window = { + tabs: new Map(), + }; + window.promise = new Promise(resolve => { + window.resolvePromise = resolve; + }); + + this.set(id, window); + } + + return super.get(id); + } + }; + + browser.tabs.onUpdated.addListener((tabId, changed, tab) => { + if (changed.status == "complete" && tab.url !== "about:blank") { + let window = windows.get(tab.windowId); + window.tabs.set(tab.index, tab); + + if (window.tabs.size === window.expectedTabs) { + window.resolvePromise(window); + } + } + }); + + async function create(options) { + let window = await browser.windows.create(options); + let win = windows.get(window.id); + + win.expectedTabs = Array.isArray(options.url) ? options.url.length : 1; + + return win.promise; + } + + try { + let windows = await Promise.all([ + create({url: REMOTE_URL}), + create({url: "test.html"}), + create({url: EXTENSION_URL}), + create({url: [REMOTE_URL, "test.html", EXTENSION_URL]}), + ]); + browser.test.assertEq(REMOTE_URL, windows[0].tabs.get(0).url, "Single, absolute, remote URL"); + + browser.test.assertEq(REMOTE_URL, windows[1].tabs.get(0).url, "Single, relative URL"); + + browser.test.assertEq(REMOTE_URL, windows[2].tabs.get(0).url, "Single, absolute, extension URL"); + + browser.test.assertEq(REMOTE_URL, windows[3].tabs.get(0).url, "url[0]: Absolute, remote URL"); + browser.test.assertEq(EXTENSION_URL, windows[3].tabs.get(1).url, "url[1]: Relative URL"); + browser.test.assertEq(EXTENSION_URL, windows[3].tabs.get(2).url, "url[2]: Absolute, extension URL"); + + browser.test.notifyPass("window-create-url"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-create-url"); + } + }, + + files: { + "test.html": `<DOCTYPE html><html><head><meta charset="utf-8"></head></html>`, + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("window-create-url"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_events.js b/browser/components/extensions/test/browser/browser_ext_windows_events.js new file mode 100644 index 000000000..dc3485b98 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_events.js @@ -0,0 +1,115 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +SimpleTest.requestCompleteLog(); + +add_task(function* testWindowsEvents() { + function background() { + browser.windows.onCreated.addListener(window => { + browser.test.log(`onCreated: windowId=${window.id}`); + + browser.test.assertTrue(Number.isInteger(window.id), + "Window object's id is an integer"); + browser.test.assertEq("normal", window.type, + "Window object returned with the correct type"); + browser.test.sendMessage("window-created", window.id); + }); + + let lastWindowId, os; + browser.windows.onFocusChanged.addListener(async windowId => { + browser.test.log(`onFocusChange: windowId=${windowId} lastWindowId=${lastWindowId}`); + + if (windowId === browser.windows.WINDOW_ID_NONE && os === "linux") { + browser.test.log("Ignoring a superfluous WINDOW_ID_NONE (blur) event on Linux"); + return; + } + + browser.test.assertTrue(lastWindowId !== windowId, + "onFocusChanged fired once for the given window"); + lastWindowId = windowId; + + browser.test.assertTrue(Number.isInteger(windowId), + "windowId is an integer"); + + let window = await browser.windows.getLastFocused(); + + browser.test.assertEq(windowId, window.id, + "Last focused window has the correct id"); + browser.test.sendMessage(`window-focus-changed`, window.id); + }); + + browser.windows.onRemoved.addListener(windowId => { + browser.test.log(`onRemoved: windowId=${windowId}`); + + browser.test.assertTrue(Number.isInteger(windowId), + "windowId is an integer"); + browser.test.sendMessage(`window-removed`, windowId); + browser.test.notifyPass("windows.events"); + }); + + browser.runtime.getPlatformInfo(info => { + os = info.os; + browser.test.sendMessage("ready"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${background})()`, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {}); + + let currentWindow = window; + let currentWindowId = WindowManager.getId(currentWindow); + info(`Current window ID: ${currentWindowId}`); + + info(`Create browser window 1`); + let win1 = yield BrowserTestUtils.openNewBrowserWindow(); + let win1Id = yield extension.awaitMessage("window-created"); + info(`Window 1 ID: ${win1Id}`); + + // This shouldn't be necessary, but tests intermittently fail, so let's give + // it a try. + win1.focus(); + + let winId = yield extension.awaitMessage(`window-focus-changed`); + is(winId, win1Id, "Got focus change event for the correct window ID."); + + info(`Create browser window 2`); + let win2 = yield BrowserTestUtils.openNewBrowserWindow(); + let win2Id = yield extension.awaitMessage("window-created"); + info(`Window 2 ID: ${win2Id}`); + + win2.focus(); + + winId = yield extension.awaitMessage(`window-focus-changed`); + is(winId, win2Id, "Got focus change event for the correct window ID."); + + info(`Focus browser window 1`); + yield focusWindow(win1); + + winId = yield extension.awaitMessage(`window-focus-changed`); + is(winId, win1Id, "Got focus change event for the correct window ID."); + + info(`Close browser window 2`); + yield BrowserTestUtils.closeWindow(win2); + + winId = yield extension.awaitMessage(`window-removed`); + is(winId, win2Id, "Got removed event for the correct window ID."); + + info(`Close browser window 1`); + yield BrowserTestUtils.closeWindow(win1); + + winId = yield extension.awaitMessage(`window-removed`); + is(winId, win1Id, "Got removed event for the correct window ID."); + + winId = yield extension.awaitMessage(`window-focus-changed`); + is(winId, currentWindowId, "Got focus change event for the correct window ID."); + + yield extension.awaitFinish("windows.events"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_size.js b/browser/components/extensions/test/browser/browser_ext_windows_size.js new file mode 100644 index 000000000..be822fea1 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_size.js @@ -0,0 +1,114 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testWindowCreate() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + let _checkWindowPromise; + browser.test.onMessage.addListener((msg, arg) => { + if (msg == "checked-window") { + _checkWindowPromise.resolve(arg); + _checkWindowPromise = null; + } + }); + + let getWindowSize = () => { + return new Promise(resolve => { + _checkWindowPromise = {resolve}; + browser.test.sendMessage("check-window"); + }); + }; + + const KEYS = ["left", "top", "width", "height"]; + function checkGeom(expected, actual) { + for (let key of KEYS) { + browser.test.assertEq(expected[key], actual[key], `Expected '${key}' value`); + } + } + + let windowId; + async function checkWindow(expected, retries = 5) { + let geom = await getWindowSize(); + + if (retries && KEYS.some(key => expected[key] != geom[key])) { + browser.test.log(`Got mismatched size (${JSON.stringify(expected)} != ${JSON.stringify(geom)}). ` + + `Retrying after a short delay.`); + + await new Promise(resolve => setTimeout(resolve, 200)); + + return checkWindow(expected, retries - 1); + } + + browser.test.log(`Check actual window size`); + checkGeom(expected, geom); + + browser.test.log("Check API-reported window size"); + + geom = await browser.windows.get(windowId); + + checkGeom(expected, geom); + } + + try { + let geom = {left: 100, top: 100, width: 500, height: 300}; + + let window = await browser.windows.create(geom); + windowId = window.id; + + await checkWindow(geom); + + let update = {left: 150, width: 600}; + Object.assign(geom, update); + await browser.windows.update(windowId, update); + await checkWindow(geom); + + update = {top: 150, height: 400}; + Object.assign(geom, update); + await browser.windows.update(windowId, update); + await checkWindow(geom); + + geom = {left: 200, top: 200, width: 800, height: 600}; + await browser.windows.update(windowId, geom); + await checkWindow(geom); + + let platformInfo = await browser.runtime.getPlatformInfo(); + if (platformInfo.os != "linux") { + geom = {left: -50, top: -50, width: 800, height: 600}; + await browser.windows.update(windowId, geom); + await checkWindow(geom); + } + + await browser.windows.remove(windowId); + browser.test.notifyPass("window-size"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-size"); + } + }, + }); + + let latestWindow; + let windowListener = (window, topic) => { + if (topic == "domwindowopened") { + latestWindow = window; + } + }; + Services.ww.registerNotification(windowListener); + + extension.onMessage("check-window", () => { + extension.sendMessage("checked-window", { + top: latestWindow.screenY, + left: latestWindow.screenX, + width: latestWindow.outerWidth, + height: latestWindow.outerHeight, + }); + }); + + yield extension.startup(); + yield extension.awaitFinish("window-size"); + yield extension.unload(); + + Services.ww.unregisterNotification(windowListener); + latestWindow = null; +}); diff --git a/browser/components/extensions/test/browser/browser_ext_windows_update.js b/browser/components/extensions/test/browser/browser_ext_windows_update.js new file mode 100644 index 000000000..b9475547a --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_windows_update.js @@ -0,0 +1,189 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* () { + function promiseWaitForFocus(window) { + return new Promise(resolve => { + waitForFocus(function() { + ok(Services.focus.activeWindow === window, "correct window focused"); + resolve(); + }, window); + }); + } + + let window1 = window; + let window2 = yield BrowserTestUtils.openNewBrowserWindow(); + + Services.focus.activeWindow = window2; + yield promiseWaitForFocus(window2); + + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.windows.getAll(undefined, function(wins) { + browser.test.assertEq(wins.length, 2, "should have two windows"); + + // Sort the unfocused window to the lower index. + wins.sort(function(win1, win2) { + if (win1.focused === win2.focused) { + return 0; + } + + return win1.focused ? 1 : -1; + }); + + browser.windows.update(wins[0].id, {focused: true}, function() { + browser.test.sendMessage("check"); + }); + }); + }, + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("check")]); + + yield promiseWaitForFocus(window1); + + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(window2); +}); + + +add_task(function* testWindowUpdate() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + let _checkWindowPromise; + browser.test.onMessage.addListener(msg => { + if (msg == "checked-window") { + _checkWindowPromise.resolve(); + _checkWindowPromise = null; + } + }); + + let os; + function checkWindow(expected) { + return new Promise(resolve => { + _checkWindowPromise = {resolve}; + browser.test.sendMessage("check-window", expected); + }); + } + + let currentWindowId; + async function updateWindow(windowId, params, expected) { + let window = await browser.windows.update(windowId, params); + + browser.test.assertEq(currentWindowId, window.id, "Expected WINDOW_ID_CURRENT to refer to the same window"); + for (let key of Object.keys(params)) { + if (key == "state" && os == "mac" && params.state == "normal") { + // OS-X doesn't have a hard distinction between "normal" and + // "maximized" states. + browser.test.assertTrue(window.state == "normal" || window.state == "maximized", + `Expected window.state (currently ${window.state}) to be "normal" but will accept "maximized"`); + } else { + browser.test.assertEq(params[key], window[key], `Got expected value for window.${key}`); + } + } + + return checkWindow(expected); + } + + try { + let windowId = browser.windows.WINDOW_ID_CURRENT; + + ({os} = await browser.runtime.getPlatformInfo()); + + let window = await browser.windows.getCurrent(); + currentWindowId = window.id; + + await updateWindow(windowId, {state: "maximized"}, {state: "STATE_MAXIMIZED"}); + await updateWindow(windowId, {state: "minimized"}, {state: "STATE_MINIMIZED"}); + await updateWindow(windowId, {state: "normal"}, {state: "STATE_NORMAL"}); + await updateWindow(windowId, {state: "fullscreen"}, {state: "STATE_FULLSCREEN"}); + await updateWindow(windowId, {state: "normal"}, {state: "STATE_NORMAL"}); + + browser.test.notifyPass("window-update"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-update"); + } + }, + }); + + extension.onMessage("check-window", expected => { + if (expected.state != null) { + let {windowState} = window; + if (window.fullScreen) { + windowState = window.STATE_FULLSCREEN; + } + + // Temporarily accepting STATE_MAXIMIZED on Linux because of bug 1307759. + if (expected.state == "STATE_NORMAL" && (AppConstants.platform == "macosx" || AppConstants.platform == "linux")) { + ok(windowState == window.STATE_NORMAL || windowState == window.STATE_MAXIMIZED, + `Expected windowState (currently ${windowState}) to be STATE_NORMAL but will accept STATE_MAXIMIZED`); + } else { + is(windowState, window[expected.state], + `Expected window state to be ${expected.state}`); + } + } + + extension.sendMessage("checked-window"); + }); + + yield extension.startup(); + yield extension.awaitFinish("window-update"); + yield extension.unload(); +}); + +add_task(function* () { + let window2 = yield BrowserTestUtils.openNewBrowserWindow(); + + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.windows.getAll(undefined, function(wins) { + browser.test.assertEq(wins.length, 2, "should have two windows"); + + let unfocused = wins.find(win => !win.focused); + browser.windows.update(unfocused.id, {drawAttention: true}, function() { + browser.test.sendMessage("check"); + }); + }); + }, + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("check")]); + + yield extension.unload(); + + yield BrowserTestUtils.closeWindow(window2); +}); + + +// Tests that incompatible parameters can't be used together. +add_task(function* testWindowUpdateParams() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + try { + for (let state of ["minimized", "maximized", "fullscreen"]) { + for (let param of ["left", "top", "width", "height"]) { + let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`; + + let windowId = browser.windows.WINDOW_ID_CURRENT; + await browser.test.assertRejects( + browser.windows.update(windowId, {state, [param]: 100}), + RegExp(expected), + `Got expected error for create(${param}=100`); + } + } + + browser.test.notifyPass("window-update-params"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("window-update-params"); + } + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("window-update-params"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/context.html b/browser/components/extensions/test/browser/context.html new file mode 100644 index 000000000..954feea52 --- /dev/null +++ b/browser/components/extensions/test/browser/context.html @@ -0,0 +1,23 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + just some text 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + <img src="ctxmenu-image.png" id="img1"> + + <p> + <a href="some-link" id="link1">Some link</a> + </p> + + <p> + <a href="image-around-some-link"> + <img src="ctxmenu-image.png" id="img-wrapped-in-link"> + </a> + </p> + + <p> + <input type="text" id="edit-me"> + </p> + </body> +</html> diff --git a/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html b/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html new file mode 100644 index 000000000..0e9b54b52 --- /dev/null +++ b/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html @@ -0,0 +1,19 @@ +<html> + <body> + <h3>test iframe</h3> + <script> + "use strict"; + + window.onload = function() { + window.onhashchange = function() { + window.parent.postMessage("updated-iframe-url", "*"); + }; + // NOTE: without the this setTimeout the location change is not fired + // even without the "fire only for top level windows" fix + setTimeout(function() { + window.location.hash = "updated-iframe-url"; + }, 0); + }; + </script> + </body> +</html> diff --git a/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html b/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html new file mode 100644 index 000000000..0f2ce1e8f --- /dev/null +++ b/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html @@ -0,0 +1,18 @@ +<html> + <body> + <h3>test page</h3> + <iframe src="about:blank"></iframe> + <script> + "use strict"; + + window.onmessage = function(evt) { + if (evt.data === "updated-iframe-url") { + window.postMessage("frame-updated", "*"); + } + }; + window.onload = function() { + document.querySelector("iframe").setAttribute("src", "context_tabs_onUpdated_iframe.html"); + }; + </script> + </body> +</html> diff --git a/browser/components/extensions/test/browser/ctxmenu-image.png b/browser/components/extensions/test/browser/ctxmenu-image.png Binary files differnew file mode 100644 index 000000000..4c3be5084 --- /dev/null +++ b/browser/components/extensions/test/browser/ctxmenu-image.png diff --git a/browser/components/extensions/test/browser/file_bypass_cache.sjs b/browser/components/extensions/test/browser/file_bypass_cache.sjs new file mode 100644 index 000000000..c91c76b88 --- /dev/null +++ b/browser/components/extensions/test/browser/file_bypass_cache.sjs @@ -0,0 +1,11 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=UTF-8", false); + + if (request.hasHeader("pragma") && request.hasHeader("cache-control")) { + response.write(`${request.getHeader("pragma")}:${request.getHeader("cache-control")}`); + } +}
\ No newline at end of file diff --git a/browser/components/extensions/test/browser/file_dummy.html b/browser/components/extensions/test/browser/file_dummy.html new file mode 100644 index 000000000..1a87e2840 --- /dev/null +++ b/browser/components/extensions/test/browser/file_dummy.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/browser/components/extensions/test/browser/file_iframe_document.html b/browser/components/extensions/test/browser/file_iframe_document.html new file mode 100644 index 000000000..fcadccf02 --- /dev/null +++ b/browser/components/extensions/test/browser/file_iframe_document.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + <iframe src="/"></iframe> +</body> +</html> diff --git a/browser/components/extensions/test/browser/file_iframe_document.sjs b/browser/components/extensions/test/browser/file_iframe_document.sjs new file mode 100644 index 000000000..661a768af --- /dev/null +++ b/browser/components/extensions/test/browser/file_iframe_document.sjs @@ -0,0 +1,41 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +// This script slows the load of an HTML document so that we can reliably test +// all phases of the load cycle supported by the extension API. + +/* eslint-disable no-unused-vars */ + +const DELAY = 1 * 1000; // Delay one second before completing the request. + +const Ci = Components.interfaces; + +let nsTimer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback"); + +let timer; + +function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write(`<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title></title> + </head> + <body> + `); + + // Note: We need to store a reference to the timer to prevent it from being + // canceled when it's GCed. + timer = new nsTimer(() => { + response.write(` + <iframe src="/"></iframe> + </body> + </html>`); + response.finish(); + }, DELAY, Ci.nsITimer.TYPE_ONE_SHOT); +} diff --git a/browser/components/extensions/test/browser/file_language_fr_en.html b/browser/components/extensions/test/browser/file_language_fr_en.html new file mode 100644 index 000000000..5e3c7b3b0 --- /dev/null +++ b/browser/components/extensions/test/browser/file_language_fr_en.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="fr"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + France is the largest country in Western Europe and the third-largest in Europe as a whole. + A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter + Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, + Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus. + Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumps over the lazy dog. +</body> +</html> diff --git a/browser/components/extensions/test/browser/file_language_ja.html b/browser/components/extensions/test/browser/file_language_ja.html new file mode 100644 index 000000000..ed07ba70e --- /dev/null +++ b/browser/components/extensions/test/browser/file_language_ja.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="ja"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + このペ ジでは アカウントに指定された予算の履歴を一覧にしています それぞれの項目には 予算額と特定期間のステ タスが表示されます 現在または今後の予算を設定するには +</body> +</html> diff --git a/browser/components/extensions/test/browser/file_language_tlh.html b/browser/components/extensions/test/browser/file_language_tlh.html new file mode 100644 index 000000000..dd7da7bdb --- /dev/null +++ b/browser/components/extensions/test/browser/file_language_tlh.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="tlh"> +<head> + <meta charset="UTF-8"> + <title></title> +</head> +<body> + tlhIngan maH! + Hab SoSlI' Quch! + Heghlu'meH QaQ jajvam +</body> +</html> diff --git a/browser/components/extensions/test/browser/file_popup_api_injection_a.html b/browser/components/extensions/test/browser/file_popup_api_injection_a.html new file mode 100644 index 000000000..750ff1db3 --- /dev/null +++ b/browser/components/extensions/test/browser/file_popup_api_injection_a.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <script type="application/javascript"> + "use strict"; + throw new Error(`WebExt Privilege Escalation: BrowserAction: typeof(browser) = ${typeof(browser)}`); + </script> +</head> +</html> diff --git a/browser/components/extensions/test/browser/file_popup_api_injection_b.html b/browser/components/extensions/test/browser/file_popup_api_injection_b.html new file mode 100644 index 000000000..b8c287e55 --- /dev/null +++ b/browser/components/extensions/test/browser/file_popup_api_injection_b.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <script type="application/javascript"> + "use strict"; + throw new Error(`WebExt Privilege Escalation: PageAction: typeof(browser) = ${typeof(browser)}`); + </script> +</head> +</html> diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js new file mode 100644 index 000000000..f8d59c944 --- /dev/null +++ b/browser/components/extensions/test/browser/head.js @@ -0,0 +1,263 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported CustomizableUI makeWidgetId focusWindow forceGC + * getBrowserActionWidget + * clickBrowserAction clickPageAction + * getBrowserActionPopup getPageActionPopup + * closeBrowserAction closePageAction + * promisePopupShown promisePopupHidden + * openContextMenu closeContextMenu + * openExtensionContextMenu closeExtensionContextMenu + * imageBuffer getListStyleImage getPanelForNode + * awaitExtensionPanel awaitPopupResize + * promiseContentDimensions alterContent + */ + +var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm"); +var {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm"); + +// Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable +// times in debug builds, which results in intermittent timeouts. Until we have +// a better solution, we force a GC after certain strategic tests, which tend to +// accumulate a high number of unreaped windows. +function forceGC() { + if (AppConstants.DEBUG) { + Cu.forceGC(); + } +} + +function makeWidgetId(id) { + id = id.toLowerCase(); + return id.replace(/[^a-z0-9_-]/g, "_"); +} + +var focusWindow = Task.async(function* focusWindow(win) { + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + if (fm.activeWindow == win) { + return; + } + + let promise = new Promise(resolve => { + win.addEventListener("focus", function listener() { + win.removeEventListener("focus", listener, true); + resolve(); + }, true); + }); + + win.focus(); + yield promise; +}); + +let img = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=="; +var imageBuffer = Uint8Array.from(atob(img), byte => byte.charCodeAt(0)).buffer; + +function getListStyleImage(button) { + let style = button.ownerDocument.defaultView.getComputedStyle(button); + + let match = /^url\("(.*)"\)$/.exec(style.listStyleImage); + + return match && match[1]; +} + +function promisePopupShown(popup) { + return new Promise(resolve => { + if (popup.state == "open") { + resolve(); + } else { + let onPopupShown = event => { + popup.removeEventListener("popupshown", onPopupShown); + resolve(); + }; + popup.addEventListener("popupshown", onPopupShown); + } + }); +} + +function promisePopupHidden(popup) { + return new Promise(resolve => { + let onPopupHidden = event => { + popup.removeEventListener("popuphidden", onPopupHidden); + resolve(); + }; + popup.addEventListener("popuphidden", onPopupHidden); + }); +} + +function promiseContentDimensions(browser) { + return ContentTask.spawn(browser, null, function* () { + function copyProps(obj, props) { + let res = {}; + for (let prop of props) { + res[prop] = obj[prop]; + } + return res; + } + + return { + window: copyProps(content, + ["innerWidth", "innerHeight", "outerWidth", "outerHeight", + "scrollX", "scrollY", "scrollMaxX", "scrollMaxY"]), + body: copyProps(content.document.body, + ["clientWidth", "clientHeight", "scrollWidth", "scrollHeight"]), + root: copyProps(content.document.documentElement, + ["clientWidth", "clientHeight", "scrollWidth", "scrollHeight"]), + + isStandards: content.document.compatMode !== "BackCompat", + }; + }); +} + +function* awaitPopupResize(browser) { + return BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized", + event => event.detail === "delayed"); +} + +function alterContent(browser, task, arg = null) { + return Promise.all([ + ContentTask.spawn(browser, arg, task), + awaitPopupResize(browser), + ]).then(() => { + return promiseContentDimensions(browser); + }); +} + +function getPanelForNode(node) { + while (node.localName != "panel") { + node = node.parentNode; + } + return node; +} + +var awaitBrowserLoaded = browser => ContentTask.spawn(browser, null, () => { + if (content.document.readyState !== "complete") { + return ContentTaskUtils.waitForEvent(content, "load").then(() => {}); + } +}); + +var awaitExtensionPanel = Task.async(function* (extension, win = window, awaitLoad = true) { + let {originalTarget: browser} = yield BrowserTestUtils.waitForEvent( + win.document, "WebExtPopupLoaded", true, + event => event.detail.extension.id === extension.id); + + yield Promise.all([ + promisePopupShown(getPanelForNode(browser)), + + awaitLoad && awaitBrowserLoaded(browser), + ]); + + return browser; +}); + +function getBrowserActionWidget(extension) { + return CustomizableUI.getWidget(makeWidgetId(extension.id) + "-browser-action"); +} + +function getBrowserActionPopup(extension, win = window) { + let group = getBrowserActionWidget(extension); + + if (group.areaType == CustomizableUI.TYPE_TOOLBAR) { + return win.document.getElementById("customizationui-widget-panel"); + } + return win.PanelUI.panel; +} + +var showBrowserAction = Task.async(function* (extension, win = window) { + let group = getBrowserActionWidget(extension); + let widget = group.forWindow(win); + + if (group.areaType == CustomizableUI.TYPE_TOOLBAR) { + ok(!widget.overflowed, "Expect widget not to be overflowed"); + } else if (group.areaType == CustomizableUI.TYPE_MENU_PANEL) { + yield win.PanelUI.show(); + } +}); + +var clickBrowserAction = Task.async(function* (extension, win = window) { + yield showBrowserAction(extension, win); + + let widget = getBrowserActionWidget(extension).forWindow(win); + + EventUtils.synthesizeMouseAtCenter(widget.node, {}, win); +}); + +function closeBrowserAction(extension, win = window) { + let group = getBrowserActionWidget(extension); + + let node = win.document.getElementById(group.viewId); + CustomizableUI.hidePanelForNode(node); + + return Promise.resolve(); +} + +function* openContextMenu(selector = "#img1") { + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown"); + yield BrowserTestUtils.synthesizeMouseAtCenter(selector, {type: "contextmenu"}, gBrowser.selectedBrowser); + yield popupShownPromise; + return contentAreaContextMenu; +} + +function* closeContextMenu() { + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden"); + contentAreaContextMenu.hidePopup(); + yield popupHiddenPromise; +} + +function* openExtensionContextMenu(selector = "#img1") { + let contextMenu = yield openContextMenu(selector); + let topLevelMenu = contextMenu.getElementsByAttribute("ext-type", "top-level-menu"); + + // Return null if the extension only has one item and therefore no extension menu. + if (topLevelMenu.length == 0) { + return null; + } + + let extensionMenu = topLevelMenu[0].childNodes[0]; + let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(extensionMenu, {}); + yield popupShownPromise; + return extensionMenu; +} + +function* closeExtensionContextMenu(itemToSelect) { + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden"); + EventUtils.synthesizeMouseAtCenter(itemToSelect, {}); + yield popupHiddenPromise; +} + +function getPageActionPopup(extension, win = window) { + let panelId = makeWidgetId(extension.id) + "-panel"; + return win.document.getElementById(panelId); +} + +function clickPageAction(extension, win = window) { + // This would normally be set automatically on navigation, and cleared + // when the user types a value into the URL bar, to show and hide page + // identity info and icons such as page action buttons. + // + // Unfortunately, that doesn't happen automatically in browser chrome + // tests. + /* globals SetPageProxyState */ + SetPageProxyState("valid"); + + let pageActionId = makeWidgetId(extension.id) + "-page-action"; + let elem = win.document.getElementById(pageActionId); + + EventUtils.synthesizeMouseAtCenter(elem, {}, win); + return new Promise(SimpleTest.executeSoon); +} + +function closePageAction(extension, win = window) { + let node = getPageActionPopup(extension, win); + if (node) { + return promisePopupShown(node).then(() => { + node.hidePopup(); + }); + } + + return Promise.resolve(); +} diff --git a/browser/components/extensions/test/browser/head_pageAction.js b/browser/components/extensions/test/browser/head_pageAction.js new file mode 100644 index 000000000..f2d81e512 --- /dev/null +++ b/browser/components/extensions/test/browser/head_pageAction.js @@ -0,0 +1,157 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported runTests */ +/* globals getListStyleImage */ + +function* runTests(options) { + function background(getTests) { + let tabs; + let tests; + + // Gets the current details of the page action, and returns a + // promise that resolves to an object containing them. + async function getDetails() { + let [tab] = await browser.tabs.query({active: true, currentWindow: true}); + let tabId = tab.id; + + browser.test.log(`Get details: tab={id: ${tabId}, url: ${JSON.stringify(tab.url)}}`); + + return { + title: await browser.pageAction.getTitle({tabId}), + popup: await browser.pageAction.getPopup({tabId}), + }; + } + + + // Runs the next test in the `tests` array, checks the results, + // and passes control back to the outer test scope. + function nextTest() { + let test = tests.shift(); + + test(async expecting => { + function finish() { + // Check that the actual icon has the expected values, then + // run the next test. + browser.test.sendMessage("nextTest", expecting, tests.length); + } + + if (expecting) { + // Check that the API returns the expected values, and then + // run the next test. + let details = await getDetails(); + + browser.test.assertEq(expecting.title, details.title, + "expected value from getTitle"); + + browser.test.assertEq(expecting.popup, details.popup, + "expected value from getPopup"); + } + + finish(); + }); + } + + async function runTests() { + tabs = []; + tests = getTests(tabs); + + let resultTabs = await browser.tabs.query({active: true, currentWindow: true}); + + tabs[0] = resultTabs[0].id; + + nextTest(); + } + + browser.test.onMessage.addListener((msg) => { + if (msg == "runTests") { + runTests(); + } else if (msg == "runNextTest") { + nextTest(); + } else { + browser.test.fail(`Unexpected message: ${msg}`); + } + }); + + runTests(); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: options.manifest, + + files: options.files || {}, + + background: `(${background})(${options.getTests})`, + }); + + let pageActionId; + let currentWindow = window; + let windows = []; + + function checkDetails(details) { + let image = currentWindow.document.getElementById(pageActionId); + if (details == null) { + ok(image == null || image.hidden, "image is hidden"); + } else { + ok(image, "image exists"); + + is(getListStyleImage(image), details.icon, "icon URL is correct"); + + let title = details.title || options.manifest.name; + is(image.getAttribute("tooltiptext"), title, "image title is correct"); + is(image.getAttribute("aria-label"), title, "image aria-label is correct"); + // TODO: Popup URL. + } + } + + let testNewWindows = 1; + + let awaitFinish = new Promise(resolve => { + extension.onMessage("nextTest", (expecting, testsRemaining) => { + if (!pageActionId) { + pageActionId = `${makeWidgetId(extension.id)}-page-action`; + } + + checkDetails(expecting); + + if (testsRemaining) { + extension.sendMessage("runNextTest"); + } else if (testNewWindows) { + testNewWindows--; + + BrowserTestUtils.openNewBrowserWindow().then(window => { + windows.push(window); + currentWindow = window; + return focusWindow(window); + }).then(() => { + extension.sendMessage("runTests"); + }); + } else { + resolve(); + } + }); + }); + + yield SpecialPowers.pushPrefEnv({set: [["general.useragent.locale", "es-ES"]]}); + + yield extension.startup(); + + yield awaitFinish; + + yield extension.unload(); + + yield SpecialPowers.popPrefEnv(); + + let node = document.getElementById(pageActionId); + is(node, null, "pageAction image removed from document"); + + currentWindow = null; + for (let win of windows.splice(0)) { + node = win.document.getElementById(pageActionId); + is(node, null, "pageAction image removed from second document"); + + yield BrowserTestUtils.closeWindow(win); + } +} + diff --git a/browser/components/extensions/test/browser/head_sessions.js b/browser/components/extensions/test/browser/head_sessions.js new file mode 100644 index 000000000..ca3a86c24 --- /dev/null +++ b/browser/components/extensions/test/browser/head_sessions.js @@ -0,0 +1,47 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported recordInitialTimestamps onlyNewItemsFilter checkRecentlyClosed */ + +let initialTimestamps = []; + +function recordInitialTimestamps(timestamps) { + initialTimestamps = timestamps; +} + +function onlyNewItemsFilter(item) { + return !initialTimestamps.includes(item.lastModified); +} + +function checkWindow(window) { + for (let prop of ["focused", "incognito", "alwaysOnTop"]) { + is(window[prop], false, `closed window has the expected value for ${prop}`); + } + for (let prop of ["state", "type"]) { + is(window[prop], "normal", `closed window has the expected value for ${prop}`); + } +} + +function checkTab(tab, windowId, incognito) { + for (let prop of ["selected", "highlighted", "active", "pinned"]) { + is(tab[prop], false, `closed tab has the expected value for ${prop}`); + } + is(tab.windowId, windowId, "closed tab has the expected value for windowId"); + is(tab.incognito, incognito, "closed tab has the expected value for incognito"); +} + +function checkRecentlyClosed(recentlyClosed, expectedCount, windowId, incognito = false) { + let sessionIds = new Set(); + is(recentlyClosed.length, expectedCount, "the expected number of closed tabs/windows was found"); + for (let item of recentlyClosed) { + if (item.window) { + sessionIds.add(item.window.sessionId); + checkWindow(item.window); + } else if (item.tab) { + sessionIds.add(item.tab.sessionId); + checkTab(item.tab, windowId, incognito); + } + } + is(sessionIds.size, expectedCount, "each item has a unique sessionId"); +} diff --git a/browser/components/extensions/test/browser/searchSuggestionEngine.sjs b/browser/components/extensions/test/browser/searchSuggestionEngine.sjs new file mode 100644 index 000000000..1978b4f66 --- /dev/null +++ b/browser/components/extensions/test/browser/searchSuggestionEngine.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + let suffixes = ["foo", "bar"]; + let data = [req.queryString, suffixes.map(s => req.queryString + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} diff --git a/browser/components/extensions/test/browser/searchSuggestionEngine.xml b/browser/components/extensions/test/browser/searchSuggestionEngine.xml new file mode 100644 index 000000000..703d45925 --- /dev/null +++ b/browser/components/extensions/test/browser/searchSuggestionEngine.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/extensions/test/browser/searchSuggestionEngine.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"/> +</SearchPlugin> diff --git a/browser/components/extensions/test/mochitest/mochitest.ini b/browser/components/extensions/test/mochitest/mochitest.ini new file mode 100644 index 000000000..39290db61 --- /dev/null +++ b/browser/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = + ../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js +tags = webextensions + +[test_ext_all_apis.html] diff --git a/browser/components/extensions/test/mochitest/test_ext_all_apis.html b/browser/components/extensions/test/mochitest/test_ext_all_apis.html new file mode 100644 index 000000000..176d380c2 --- /dev/null +++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; +/* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */ +let expectedContentApisTargetSpecific = [ +]; + +let expectedBackgroundApisTargetSpecific = [ + "tabs.MutedInfoReason", + "tabs.TAB_ID_NONE", + "tabs.TabStatus", + "tabs.WindowType", + "tabs.ZoomSettingsMode", + "tabs.ZoomSettingsScope", + "tabs.connect", + "tabs.create", + "tabs.detectLanguage", + "tabs.duplicate", + "tabs.executeScript", + "tabs.get", + "tabs.getCurrent", + "tabs.getZoom", + "tabs.getZoomSettings", + "tabs.highlight", + "tabs.insertCSS", + "tabs.move", + "tabs.onActivated", + "tabs.onAttached", + "tabs.onCreated", + "tabs.onDetached", + "tabs.onHighlighted", + "tabs.onMoved", + "tabs.onRemoved", + "tabs.onReplaced", + "tabs.onUpdated", + "tabs.onZoomChange", + "tabs.query", + "tabs.reload", + "tabs.remove", + "tabs.removeCSS", + "tabs.sendMessage", + "tabs.setZoom", + "tabs.setZoomSettings", + "tabs.update", + "windows.CreateType", + "windows.WINDOW_ID_CURRENT", + "windows.WINDOW_ID_NONE", + "windows.WindowState", + "windows.WindowType", + "windows.create", + "windows.get", + "windows.getAll", + "windows.getCurrent", + "windows.getLastFocused", + "windows.onCreated", + "windows.onFocusChanged", + "windows.onRemoved", + "windows.remove", + "windows.update", +]; +</script> +<script src="test_ext_all_apis.js"></script> + +</body> +</html> diff --git a/browser/components/extensions/test/xpcshell/.eslintrc.js b/browser/components/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 000000000..2bfe540ea --- /dev/null +++ b/browser/components/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { // eslint-disable-line no-undef + "extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js", + + "globals": { + "browser": false, + }, +}; diff --git a/browser/components/extensions/test/xpcshell/head.js b/browser/components/extensions/test/xpcshell/head.js new file mode 100644 index 000000000..de4a4a3f6 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/head.js @@ -0,0 +1,55 @@ +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +/* exported createHttpServer */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Extension", + "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", + "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", + "resource://gre/modules/ExtensionManagement.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils", + "resource://testing-common/ExtensionXPCShellUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", + "resource://testing-common/httpd.js"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Schemas", + "resource://gre/modules/Schemas.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +ExtensionTestUtils.init(this); + + +/** + * Creates a new HttpServer for testing, and begins listening on the + * specified port. Automatically shuts down the server when the test + * unit ends. + * + * @param {integer} [port] + * The port to listen on. If omitted, listen on a random + * port. The latter is the preferred behavior. + * + * @returns {HttpServer} + */ +function createHttpServer(port = -1) { + let server = new HttpServer(); + server.start(port); + + do_register_cleanup(() => { + return new Promise(resolve => { + server.stop(resolve); + }); + }); + + return server; +} diff --git a/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js new file mode 100644 index 000000000..142c0a37c --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js @@ -0,0 +1,601 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function backgroundScript() { + let unsortedId, ourId; + let initialBookmarkCount = 0; + let createdBookmarks = new Set(); + let createdFolderId; + let collectedEvents = []; + const nonExistentId = "000000000000"; + const bookmarkGuids = { + menuGuid: "menu________", + toolbarGuid: "toolbar_____", + unfiledGuid: "unfiled_____", + }; + + function checkOurBookmark(bookmark) { + browser.test.assertEq(ourId, bookmark.id, "Bookmark has the expected Id"); + browser.test.assertTrue("parentId" in bookmark, "Bookmark has a parentId"); + browser.test.assertEq(0, bookmark.index, "Bookmark has the expected index"); // We assume there are no other bookmarks. + browser.test.assertEq("http://example.org/", bookmark.url, "Bookmark has the expected url"); + browser.test.assertEq("test bookmark", bookmark.title, "Bookmark has the expected title"); + browser.test.assertTrue("dateAdded" in bookmark, "Bookmark has a dateAdded"); + browser.test.assertFalse("dateGroupModified" in bookmark, "Bookmark does not have a dateGroupModified"); + browser.test.assertFalse("unmodifiable" in bookmark, "Bookmark is not unmodifiable"); + } + + function checkBookmark(expected, bookmark) { + browser.test.assertEq(expected.url, bookmark.url, "Bookmark has the expected url"); + browser.test.assertEq(expected.title, bookmark.title, "Bookmark has the expected title"); + browser.test.assertEq(expected.index, bookmark.index, "Bookmark has expected index"); + if ("parentId" in expected) { + browser.test.assertEq(expected.parentId, bookmark.parentId, "Bookmark has the expected parentId"); + } + } + + function expectedError() { + browser.test.fail("Did not get expected error"); + } + + function checkOnCreated(id, parentId, index, title, url, dateAdded) { + let createdData = collectedEvents.pop(); + browser.test.assertEq("onCreated", createdData.event, "onCreated was the last event received"); + browser.test.assertEq(id, createdData.id, "onCreated event received the expected id"); + let bookmark = createdData.bookmark; + browser.test.assertEq(id, bookmark.id, "onCreated event received the expected bookmark id"); + browser.test.assertEq(parentId, bookmark.parentId, "onCreated event received the expected bookmark parentId"); + browser.test.assertEq(index, bookmark.index, "onCreated event received the expected bookmark index"); + browser.test.assertEq(title, bookmark.title, "onCreated event received the expected bookmark title"); + browser.test.assertEq(url, bookmark.url, "onCreated event received the expected bookmark url"); + browser.test.assertEq(dateAdded, bookmark.dateAdded, "onCreated event received the expected bookmark dateAdded"); + } + + function checkOnChanged(id, url, title) { + // If both url and title are changed, then url is fired last. + let changedData = collectedEvents.pop(); + browser.test.assertEq("onChanged", changedData.event, "onChanged was the last event received"); + browser.test.assertEq(id, changedData.id, "onChanged event received the expected id"); + browser.test.assertEq(url, changedData.info.url, "onChanged event received the expected url"); + // title is fired first. + changedData = collectedEvents.pop(); + browser.test.assertEq("onChanged", changedData.event, "onChanged was the last event received"); + browser.test.assertEq(id, changedData.id, "onChanged event received the expected id"); + browser.test.assertEq(title, changedData.info.title, "onChanged event received the expected title"); + } + + function checkOnMoved(id, parentId, oldParentId, index, oldIndex) { + let movedData = collectedEvents.pop(); + browser.test.assertEq("onMoved", movedData.event, "onMoved was the last event received"); + browser.test.assertEq(id, movedData.id, "onMoved event received the expected id"); + let info = movedData.info; + browser.test.assertEq(parentId, info.parentId, "onMoved event received the expected parentId"); + browser.test.assertEq(oldParentId, info.oldParentId, "onMoved event received the expected oldParentId"); + browser.test.assertEq(index, info.index, "onMoved event received the expected index"); + browser.test.assertEq(oldIndex, info.oldIndex, "onMoved event received the expected oldIndex"); + } + + function checkOnRemoved(id, parentId, index, url) { + let removedData = collectedEvents.pop(); + browser.test.assertEq("onRemoved", removedData.event, "onRemoved was the last event received"); + browser.test.assertEq(id, removedData.id, "onRemoved event received the expected id"); + let info = removedData.info; + browser.test.assertEq(parentId, removedData.info.parentId, "onRemoved event received the expected parentId"); + browser.test.assertEq(index, removedData.info.index, "onRemoved event received the expected index"); + let node = info.node; + browser.test.assertEq(id, node.id, "onRemoved event received the expected node id"); + browser.test.assertEq(parentId, node.parentId, "onRemoved event received the expected node parentId"); + browser.test.assertEq(index, node.index, "onRemoved event received the expected node index"); + browser.test.assertEq(url, node.url, "onRemoved event received the expected node url"); + } + + browser.bookmarks.onChanged.addListener((id, info) => { + collectedEvents.push({event: "onChanged", id, info}); + }); + + browser.bookmarks.onCreated.addListener((id, bookmark) => { + collectedEvents.push({event: "onCreated", id, bookmark}); + }); + + browser.bookmarks.onMoved.addListener((id, info) => { + collectedEvents.push({event: "onMoved", id, info}); + }); + + browser.bookmarks.onRemoved.addListener((id, info) => { + collectedEvents.push({event: "onRemoved", id, info}); + }); + + browser.bookmarks.get(["not-a-bookmark-guid"]).then(expectedError, invalidGuidError => { + browser.test.assertTrue( + invalidGuidError.message.includes("Invalid value for property 'guid': not-a-bookmark-guid"), + "Expected error thrown when trying to get a bookmark using an invalid guid" + ); + + return browser.bookmarks.get([nonExistentId]).then(expectedError, nonExistentIdError => { + browser.test.assertTrue( + nonExistentIdError.message.includes("Bookmark not found"), + "Expected error thrown when trying to get a bookmark using a non-existent Id" + ); + }); + }).then(() => { + return browser.bookmarks.search({}); + }).then(results => { + initialBookmarkCount = results.length; + return browser.bookmarks.create({title: "test bookmark", url: "http://example.org"}); + }).then(result => { + ourId = result.id; + checkOurBookmark(result); + browser.test.assertEq(1, collectedEvents.length, "1 expected event received"); + checkOnCreated(ourId, bookmarkGuids.unfiledGuid, 0, "test bookmark", "http://example.org/", result.dateAdded); + + return browser.bookmarks.get(ourId); + }).then(results => { + browser.test.assertEq(results.length, 1); + checkOurBookmark(results[0]); + + unsortedId = results[0].parentId; + return browser.bookmarks.get(unsortedId); + }).then(results => { + let folder = results[0]; + browser.test.assertEq(1, results.length, "1 bookmark was returned"); + + browser.test.assertEq(unsortedId, folder.id, "Folder has the expected id"); + browser.test.assertTrue("parentId" in folder, "Folder has a parentId"); + browser.test.assertTrue("index" in folder, "Folder has an index"); + browser.test.assertFalse("url" in folder, "Folder does not have a url"); + browser.test.assertEq("Other Bookmarks", folder.title, "Folder has the expected title"); + browser.test.assertTrue("dateAdded" in folder, "Folder has a dateAdded"); + browser.test.assertTrue("dateGroupModified" in folder, "Folder has a dateGroupModified"); + browser.test.assertFalse("unmodifiable" in folder, "Folder is not unmodifiable"); // TODO: Do we want to enable this? + + return browser.bookmarks.getChildren(unsortedId); + }).then(results => { + browser.test.assertEq(1, results.length, "The folder has one child"); + checkOurBookmark(results[0]); + + return browser.bookmarks.update(nonExistentId, {title: "new test title"}).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("No bookmarks found for the provided GUID"), + "Expected error thrown when trying to update a non-existent bookmark" + ); + + return browser.bookmarks.update(ourId, {title: "new test title", url: "http://example.com/"}); + }); + }).then(result => { + browser.test.assertEq("new test title", result.title, "Updated bookmark has the expected title"); + browser.test.assertEq("http://example.com/", result.url, "Updated bookmark has the expected URL"); + browser.test.assertEq(ourId, result.id, "Updated bookmark has the expected id"); + + browser.test.assertEq(2, collectedEvents.length, "2 expected events received"); + checkOnChanged(ourId, "http://example.com/", "new test title"); + + return Promise.resolve().then(() => { + return browser.bookmarks.update(ourId, {url: "this is not a valid url"}); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Invalid bookmark:"), + "Expected error thrown when trying update with an invalid url" + ); + return browser.bookmarks.getTree(); + }); + }).then(results => { + browser.test.assertEq(1, results.length, "getTree returns one result"); + let bookmark = results[0].children.find(bookmarkItem => bookmarkItem.id == unsortedId); + browser.test.assertEq( + "Other Bookmarks", + bookmark.title, + "Folder returned from getTree has the expected title" + ); + + return browser.bookmarks.create({parentId: "invalid"}).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Invalid bookmark"), + "Expected error thrown when trying to create a bookmark with an invalid parentId" + ); + browser.test.assertTrue( + error.message.includes(`"parentGuid":"invalid"`), + "Expected error thrown when trying to create a bookmark with an invalid parentId" + ); + }); + }).then(() => { + return browser.bookmarks.remove(ourId); + }).then(result => { + browser.test.assertEq(undefined, result, "Removing a bookmark returns undefined"); + + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnRemoved(ourId, bookmarkGuids.unfiledGuid, 0, "http://example.com/"); + + return browser.bookmarks.get(ourId).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Bookmark not found"), + "Expected error thrown when trying to get a removed bookmark" + ); + }); + }).then(() => { + return browser.bookmarks.remove(nonExistentId).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("No bookmarks found for the provided GUID"), + "Expected error thrown when trying removed a non-existent bookmark" + ); + }); + }).then(() => { + // test bookmarks.search + return Promise.all([ + browser.bookmarks.create({title: "MØzillä", url: "http://møzîllä.örg/"}), + browser.bookmarks.create({title: "Example", url: "http://example.org/"}), + browser.bookmarks.create({title: "Mozilla Folder"}), + browser.bookmarks.create({title: "EFF", url: "http://eff.org/"}), + browser.bookmarks.create({title: "Menu Item", url: "http://menu.org/", parentId: bookmarkGuids.menuGuid}), + browser.bookmarks.create({title: "Toolbar Item", url: "http://toolbar.org/", parentId: bookmarkGuids.toolbarGuid}), + ]); + }).then(results => { + browser.test.assertEq(6, collectedEvents.length, "6 expected events received"); + checkOnCreated(results[5].id, bookmarkGuids.toolbarGuid, 0, "Toolbar Item", "http://toolbar.org/", results[5].dateAdded); + checkOnCreated(results[4].id, bookmarkGuids.menuGuid, 0, "Menu Item", "http://menu.org/", results[4].dateAdded); + checkOnCreated(results[3].id, bookmarkGuids.unfiledGuid, 0, "EFF", "http://eff.org/", results[3].dateAdded); + checkOnCreated(results[2].id, bookmarkGuids.unfiledGuid, 0, "Mozilla Folder", undefined, results[2].dateAdded); + checkOnCreated(results[1].id, bookmarkGuids.unfiledGuid, 0, "Example", "http://example.org/", results[1].dateAdded); + checkOnCreated(results[0].id, bookmarkGuids.unfiledGuid, 0, "MØzillä", "http://møzîllä.örg/", results[0].dateAdded); + + for (let result of results) { + if (result.title !== "Mozilla Folder") { + createdBookmarks.add(result.id); + } + } + let folderResult = results[2]; + createdFolderId = folderResult.id; + return Promise.all([ + browser.bookmarks.create({title: "Mozilla", url: "http://allizom.org/", parentId: createdFolderId}), + browser.bookmarks.create({title: "Mozilla Corporation", url: "http://allizom.com/", parentId: createdFolderId}), + browser.bookmarks.create({title: "Firefox", url: "http://allizom.org/firefox/", parentId: createdFolderId}), + ]).then(newBookmarks => { + browser.test.assertEq(3, collectedEvents.length, "3 expected events received"); + checkOnCreated(newBookmarks[2].id, createdFolderId, 0, "Firefox", "http://allizom.org/firefox/", newBookmarks[2].dateAdded); + checkOnCreated(newBookmarks[1].id, createdFolderId, 0, "Mozilla Corporation", "http://allizom.com/", newBookmarks[1].dateAdded); + checkOnCreated(newBookmarks[0].id, createdFolderId, 0, "Mozilla", "http://allizom.org/", newBookmarks[0].dateAdded); + + return browser.bookmarks.create({ + title: "About Mozilla", + url: "http://allizom.org/about/", + parentId: createdFolderId, + index: 1, + }); + }).then(result => { + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnCreated(result.id, createdFolderId, 1, "About Mozilla", "http://allizom.org/about/", result.dateAdded); + + // returns all items on empty object + return browser.bookmarks.search({}); + }).then(bookmarksSearchResults => { + browser.test.assertTrue(bookmarksSearchResults.length >= 9, "At least as many bookmarks as added were returned by search({})"); + + return Promise.resolve().then(() => { + return browser.bookmarks.remove(createdFolderId); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Cannot remove a non-empty folder"), + "Expected error thrown when trying to remove a non-empty folder" + ); + return browser.bookmarks.getSubTree(createdFolderId); + }); + }); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of nodes returned by getSubTree"); + browser.test.assertEq("Mozilla Folder", results[0].title, "Folder has the expected title"); + let children = results[0].children; + browser.test.assertEq(4, children.length, "Expected number of bookmarks returned by getSubTree"); + browser.test.assertEq("Firefox", children[0].title, "Bookmark has the expected title"); + browser.test.assertEq("About Mozilla", children[1].title, "Bookmark has the expected title"); + browser.test.assertEq(1, children[1].index, "Bookmark has the expected index"); + browser.test.assertEq("Mozilla Corporation", children[2].title, "Bookmark has the expected title"); + browser.test.assertEq("Mozilla", children[3].title, "Bookmark has the expected title"); + + // throws an error for invalid query objects + Promise.resolve().then(() => { + return browser.bookmarks.search(); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Incorrect argument types for bookmarks.search"), + "Expected error thrown when trying to search with no arguments" + ); + }); + + Promise.resolve().then(() => { + return browser.bookmarks.search(null); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Incorrect argument types for bookmarks.search"), + "Expected error thrown when trying to search with null as an argument" + ); + }); + + Promise.resolve().then(() => { + return browser.bookmarks.search(function() {}); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Incorrect argument types for bookmarks.search"), + "Expected error thrown when trying to search with a function as an argument" + ); + }); + + Promise.resolve().then(() => { + return browser.bookmarks.search({banana: "banana"}); + }).then(expectedError, error => { + let substr = `an unexpected "banana" property`; + browser.test.assertTrue( + error.message.includes(substr), + `Expected error ${JSON.stringify(error.message)} to contain ${JSON.stringify(substr)}`); + }); + + Promise.resolve().then(() => { + return browser.bookmarks.search({url: "spider-man vs. batman"}); + }).then(expectedError, error => { + let substr = 'must match the format "url"'; + browser.test.assertTrue( + error.message.includes(substr), + `Expected error ${JSON.stringify(error.message)} to contain ${JSON.stringify(substr)}`); + }); + + // queries the full url + return browser.bookmarks.search("http://example.org/"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for url search"); + checkBookmark({title: "Example", url: "http://example.org/", index: 2}, results[0]); + + // queries a partial url + return browser.bookmarks.search("example.org"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for url search"); + checkBookmark({title: "Example", url: "http://example.org/", index: 2}, results[0]); + + // queries the title + return browser.bookmarks.search("EFF"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for title search"); + checkBookmark({title: "EFF", url: "http://eff.org/", index: 0, parentId: bookmarkGuids.unfiledGuid}, results[0]); + + // finds menu items + return browser.bookmarks.search("Menu Item"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for menu item search"); + checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 0, parentId: bookmarkGuids.menuGuid}, results[0]); + + // finds toolbar items + return browser.bookmarks.search("Toolbar Item"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for toolbar item search"); + checkBookmark({title: "Toolbar Item", url: "http://toolbar.org/", index: 0, parentId: bookmarkGuids.toolbarGuid}, results[0]); + + // finds folders + return browser.bookmarks.search("Mozilla Folder"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of folders returned"); + browser.test.assertEq("Mozilla Folder", results[0].title, "Folder has the expected title"); + + // is case-insensitive + return browser.bookmarks.search("corporation"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returnedfor case-insensitive search"); + browser.test.assertEq("Mozilla Corporation", results[0].title, "Bookmark has the expected title"); + + // is case-insensitive for non-ascii + return browser.bookmarks.search("MøZILLÄ"); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for non-ascii search"); + browser.test.assertEq("MØzillä", results[0].title, "Bookmark has the expected title"); + + // returns multiple results + return browser.bookmarks.search("allizom"); + }).then(results => { + browser.test.assertEq(4, results.length, "Expected number of multiple results returned"); + browser.test.assertEq("Mozilla", results[0].title, "Bookmark has the expected title"); + browser.test.assertEq("Mozilla Corporation", results[1].title, "Bookmark has the expected title"); + browser.test.assertEq("Firefox", results[2].title, "Bookmark has the expected title"); + browser.test.assertEq("About Mozilla", results[3].title, "Bookmark has the expected title"); + + // accepts a url field + return browser.bookmarks.search({url: "http://allizom.com/"}); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for url field"); + checkBookmark({title: "Mozilla Corporation", url: "http://allizom.com/", index: 2}, results[0]); + + // normalizes urls + return browser.bookmarks.search({url: "http://allizom.com"}); + }).then(results => { + browser.test.assertEq(results.length, 1, "Expected number of results returned for normalized url field"); + checkBookmark({title: "Mozilla Corporation", url: "http://allizom.com/", index: 2}, results[0]); + + // normalizes urls even more + return browser.bookmarks.search({url: "http:allizom.com"}); + }).then(results => { + browser.test.assertEq(results.length, 1, "Expected number of results returned for normalized url field"); + checkBookmark({title: "Mozilla Corporation", url: "http://allizom.com/", index: 2}, results[0]); + + // accepts a title field + return browser.bookmarks.search({title: "Mozilla"}); + }).then(results => { + browser.test.assertEq(results.length, 1, "Expected number of results returned for title field"); + checkBookmark({title: "Mozilla", url: "http://allizom.org/", index: 3}, results[0]); + + // can combine title and query + return browser.bookmarks.search({title: "Mozilla", query: "allizom"}); + }).then(results => { + browser.test.assertEq(1, results.length, "Expected number of results returned for title and query fields"); + checkBookmark({title: "Mozilla", url: "http://allizom.org/", index: 3}, results[0]); + + // uses AND conditions + return browser.bookmarks.search({title: "EFF", query: "allizom"}); + }).then(results => { + browser.test.assertEq( + 0, + results.length, + "Expected number of results returned for non-matching title and query fields" + ); + + // returns an empty array on item not found + return browser.bookmarks.search("microsoft"); + }).then(results => { + browser.test.assertEq(0, results.length, "Expected number of results returned for non-matching search"); + + return Promise.resolve().then(() => { + return browser.bookmarks.getRecent(""); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Incorrect argument types for bookmarks.getRecent"), + "Expected error thrown when calling getRecent with an empty string" + ); + }); + }).then(() => { + return Promise.resolve().then(() => { + return browser.bookmarks.getRecent(1.234); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Incorrect argument types for bookmarks.getRecent"), + "Expected error thrown when calling getRecent with a decimal number" + ); + }); + }).then(() => { + return Promise.all([ + browser.bookmarks.search("corporation"), + browser.bookmarks.getChildren(bookmarkGuids.menuGuid), + ]); + }).then(results => { + let corporationBookmark = results[0][0]; + let childCount = results[1].length; + + browser.test.assertEq(2, corporationBookmark.index, "Bookmark has the expected index"); + + return browser.bookmarks.move(corporationBookmark.id, {index: 0}).then(result => { + browser.test.assertEq(0, result.index, "Bookmark has the expected index"); + + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnMoved(corporationBookmark.id, createdFolderId, createdFolderId, 0, 2); + + return browser.bookmarks.move(corporationBookmark.id, {parentId: bookmarkGuids.menuGuid}); + }).then(result => { + browser.test.assertEq(bookmarkGuids.menuGuid, result.parentId, "Bookmark has the expected parent"); + browser.test.assertEq(childCount, result.index, "Bookmark has the expected index"); + + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnMoved(corporationBookmark.id, bookmarkGuids.menuGuid, createdFolderId, 1, 0); + + return browser.bookmarks.move(corporationBookmark.id, {index: 0}); + }).then(result => { + browser.test.assertEq(bookmarkGuids.menuGuid, result.parentId, "Bookmark has the expected parent"); + browser.test.assertEq(0, result.index, "Bookmark has the expected index"); + + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnMoved(corporationBookmark.id, bookmarkGuids.menuGuid, bookmarkGuids.menuGuid, 0, 1); + + return browser.bookmarks.move(corporationBookmark.id, {parentId: bookmarkGuids.toolbarGuid, index: 1}); + }).then(result => { + browser.test.assertEq(bookmarkGuids.toolbarGuid, result.parentId, "Bookmark has the expected parent"); + browser.test.assertEq(1, result.index, "Bookmark has the expected index"); + + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnMoved(corporationBookmark.id, bookmarkGuids.toolbarGuid, bookmarkGuids.menuGuid, 1, 0); + + createdBookmarks.add(corporationBookmark.id); + }); + }).then(() => { + return browser.bookmarks.getRecent(4); + }).then(results => { + browser.test.assertEq(4, results.length, "Expected number of results returned by getRecent"); + let prevDate = results[0].dateAdded; + for (let bookmark of results) { + browser.test.assertTrue(bookmark.dateAdded <= prevDate, "The recent bookmarks are sorted by dateAdded"); + prevDate = bookmark.dateAdded; + } + let bookmarksByTitle = results.sort((a, b) => { + return a.title.localeCompare(b.title); + }); + browser.test.assertEq("About Mozilla", bookmarksByTitle[0].title, "Bookmark has the expected title"); + browser.test.assertEq("Firefox", bookmarksByTitle[1].title, "Bookmark has the expected title"); + browser.test.assertEq("Mozilla", bookmarksByTitle[2].title, "Bookmark has the expected title"); + browser.test.assertEq("Mozilla Corporation", bookmarksByTitle[3].title, "Bookmark has the expected title"); + + return browser.bookmarks.search({}); + }).then(results => { + let startBookmarkCount = results.length; + + return browser.bookmarks.search({title: "Mozilla Folder"}).then(result => { + return browser.bookmarks.removeTree(result[0].id); + }).then(() => { + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnRemoved(createdFolderId, bookmarkGuids.unfiledGuid, 1); + + return browser.bookmarks.search({}).then(searchResults => { + browser.test.assertEq( + startBookmarkCount - 4, + searchResults.length, + "Expected number of results returned after removeTree"); + }); + }); + }).then(() => { + return browser.bookmarks.create({title: "Empty Folder"}); + }).then(result => { + let emptyFolderId = result.id; + + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnCreated(emptyFolderId, bookmarkGuids.unfiledGuid, 3, "Empty Folder", undefined, result.dateAdded); + + browser.test.assertEq("Empty Folder", result.title, "Folder has the expected title"); + return browser.bookmarks.remove(emptyFolderId).then(() => { + browser.test.assertEq(1, collectedEvents.length, "1 expected events received"); + checkOnRemoved(emptyFolderId, bookmarkGuids.unfiledGuid, 3); + + return browser.bookmarks.get(emptyFolderId).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("Bookmark not found"), + "Expected error thrown when trying to get a removed folder" + ); + }); + }); + }).then(() => { + return browser.bookmarks.getChildren(nonExistentId).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("root is null"), + "Expected error thrown when trying to getChildren for a non-existent folder" + ); + }); + }).then(() => { + return Promise.resolve().then(() => { + return browser.bookmarks.move(nonExistentId, {}); + }).then(expectedError, error => { + browser.test.assertTrue( + error.message.includes("No bookmarks found for the provided GUID"), + "Expected error thrown when calling move with a non-existent bookmark" + ); + }); + }).then(() => { + // remove all created bookmarks + let promises = Array.from(createdBookmarks, guid => browser.bookmarks.remove(guid)); + return Promise.all(promises); + }).then(() => { + browser.test.assertEq(createdBookmarks.size, collectedEvents.length, "expected number of events received"); + + return browser.bookmarks.search({}); + }).then(results => { + browser.test.assertEq(initialBookmarkCount, results.length, "All created bookmarks have been removed"); + + return browser.test.notifyPass("bookmarks"); + }).catch(error => { + browser.test.fail(`Error: ${String(error)} :: ${error.stack}`); + browser.test.notifyFail("bookmarks"); + }); +} + +let extensionData = { + background: `(${backgroundScript})()`, + manifest: { + permissions: ["bookmarks"], + }, +}; + +add_task(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitFinish("bookmarks"); + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_history.js b/browser/components/extensions/test/xpcshell/test_ext_history.js new file mode 100644 index 000000000..78df33151 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_history.js @@ -0,0 +1,487 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", + "resource://gre/modules/ExtensionUtils.jsm"); + +add_task(function* test_delete() { + function background() { + let historyClearedCount = 0; + let removedUrls = []; + + browser.history.onVisitRemoved.addListener(data => { + if (data.allHistory) { + historyClearedCount++; + browser.test.assertEq(0, data.urls.length, "onVisitRemoved received an empty urls array"); + } else { + browser.test.assertEq(1, data.urls.length, "onVisitRemoved received one URL"); + removedUrls.push(data.urls[0]); + } + }); + + browser.test.onMessage.addListener((msg, arg) => { + if (msg === "delete-url") { + browser.history.deleteUrl({url: arg}).then(result => { + browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing"); + browser.test.sendMessage("url-deleted"); + }); + } else if (msg === "delete-range") { + browser.history.deleteRange(arg).then(result => { + browser.test.assertEq(undefined, result, "browser.history.deleteRange returns nothing"); + browser.test.sendMessage("range-deleted", removedUrls); + }); + } else if (msg === "delete-all") { + browser.history.deleteAll().then(result => { + browser.test.assertEq(undefined, result, "browser.history.deleteAll returns nothing"); + browser.test.sendMessage("history-cleared", [historyClearedCount, removedUrls]); + }); + } + }); + + browser.test.sendMessage("ready"); + } + + const BASE_URL = "http://mozilla.com/test_history/"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + yield PlacesTestUtils.clearHistory(); + + let historyClearedCount; + let visits = []; + let visitDate = new Date(1999, 9, 9, 9, 9).getTime(); + + function pushVisit(subvisits) { + visitDate += 1000; + subvisits.push({date: new Date(visitDate)}); + } + + // Add 5 visits for one uri and 3 visits for 3 others + for (let i = 0; i < 4; ++i) { + let visit = { + url: `${BASE_URL}${i}`, + title: "visit " + i, + visits: [], + }; + if (i === 0) { + for (let j = 0; j < 5; ++j) { + pushVisit(visit.visits); + } + } else { + pushVisit(visit.visits); + } + visits.push(visit); + } + + yield PlacesUtils.history.insertMany(visits); + equal((yield PlacesTestUtils.visitsInDB(visits[0].url)), 5, "5 visits for uri found in history database"); + + let testUrl = visits[2].url; + ok((yield PlacesTestUtils.isPageInDB(testUrl)), "expected url found in history database"); + + extension.sendMessage("delete-url", testUrl); + yield extension.awaitMessage("url-deleted"); + equal((yield PlacesTestUtils.isPageInDB(testUrl)), false, "expected url not found in history database"); + + // delete 3 of the 5 visits for url 1 + let filter = { + startTime: visits[0].visits[0].date, + endTime: visits[0].visits[2].date, + }; + + extension.sendMessage("delete-range", filter); + let removedUrls = yield extension.awaitMessage("range-deleted"); + ok(!removedUrls.includes(visits[0].url), `${visits[0].url} not received by onVisitRemoved`); + ok((yield PlacesTestUtils.isPageInDB(visits[0].url)), "expected uri found in history database"); + equal((yield PlacesTestUtils.visitsInDB(visits[0].url)), 2, "2 visits for uri found in history database"); + ok((yield PlacesTestUtils.isPageInDB(visits[1].url)), "expected uri found in history database"); + equal((yield PlacesTestUtils.visitsInDB(visits[1].url)), 1, "1 visit for uri found in history database"); + + // delete the rest of the visits for url 1, and the visit for url 2 + filter.startTime = visits[0].visits[0].date; + filter.endTime = visits[1].visits[0].date; + + extension.sendMessage("delete-range", filter); + yield extension.awaitMessage("range-deleted"); + + equal((yield PlacesTestUtils.isPageInDB(visits[0].url)), false, "expected uri not found in history database"); + equal((yield PlacesTestUtils.visitsInDB(visits[0].url)), 0, "0 visits for uri found in history database"); + equal((yield PlacesTestUtils.isPageInDB(visits[1].url)), false, "expected uri not found in history database"); + equal((yield PlacesTestUtils.visitsInDB(visits[1].url)), 0, "0 visits for uri found in history database"); + + ok((yield PlacesTestUtils.isPageInDB(visits[3].url)), "expected uri found in history database"); + + extension.sendMessage("delete-all"); + [historyClearedCount, removedUrls] = yield extension.awaitMessage("history-cleared"); + equal(PlacesUtils.history.hasHistoryEntries, false, "history is empty"); + equal(historyClearedCount, 2, "onVisitRemoved called for each clearing of history"); + equal(removedUrls.length, 3, "onVisitRemoved called the expected number of times"); + for (let i = 1; i < 3; ++i) { + let url = visits[i].url; + ok(removedUrls.includes(url), `${url} received by onVisitRemoved`); + } + yield extension.unload(); +}); + +add_task(function* test_search() { + const SINGLE_VISIT_URL = "http://example.com/"; + const DOUBLE_VISIT_URL = "http://example.com/2/"; + const MOZILLA_VISIT_URL = "http://mozilla.com/"; + const REFERENCE_DATE = new Date(); + // pages/visits to add via History.insert + const PAGE_INFOS = [ + { + url: SINGLE_VISIT_URL, + title: `test visit for ${SINGLE_VISIT_URL}`, + visits: [ + {date: new Date(Number(REFERENCE_DATE) - 1000)}, + ], + }, + { + url: DOUBLE_VISIT_URL, + title: `test visit for ${DOUBLE_VISIT_URL}`, + visits: [ + {date: REFERENCE_DATE}, + {date: new Date(Number(REFERENCE_DATE) - 2000)}, + ], + }, + { + url: MOZILLA_VISIT_URL, + title: `test visit for ${MOZILLA_VISIT_URL}`, + visits: [ + {date: new Date(Number(REFERENCE_DATE) - 3000)}, + ], + }, + ]; + + function background(BGSCRIPT_REFERENCE_DATE) { + const futureTime = Date.now() + 24 * 60 * 60 * 1000; + + browser.test.onMessage.addListener(msg => { + browser.history.search({text: ""}).then(results => { + browser.test.sendMessage("empty-search", results); + return browser.history.search({text: "mozilla.com"}); + }).then(results => { + browser.test.sendMessage("text-search", results); + return browser.history.search({text: "example.com", maxResults: 1}); + }).then(results => { + browser.test.sendMessage("max-results-search", results); + return browser.history.search({text: "", startTime: BGSCRIPT_REFERENCE_DATE - 2000, endTime: BGSCRIPT_REFERENCE_DATE - 1000}); + }).then(results => { + browser.test.sendMessage("date-range-search", results); + return browser.history.search({text: "", startTime: futureTime}); + }).then(results => { + browser.test.assertEq(0, results.length, "no results returned for late start time"); + return browser.history.search({text: "", endTime: 0}); + }).then(results => { + browser.test.assertEq(0, results.length, "no results returned for early end time"); + return browser.history.search({text: "", startTime: Date.now(), endTime: 0}); + }).then(results => { + browser.test.fail("history.search rejects with startTime that is after the endTime"); + }, error => { + browser.test.assertEq( + "The startTime cannot be after the endTime", + error.message, + "history.search rejects with startTime that is after the endTime"); + }).then(() => { + browser.test.notifyPass("search"); + }); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})(${Number(REFERENCE_DATE)})`, + }); + + function findResult(url, results) { + return results.find(r => r.url === url); + } + + function checkResult(results, url, expectedCount) { + let result = findResult(url, results); + notEqual(result, null, `history.search result was found for ${url}`); + equal(result.visitCount, expectedCount, `history.search reports ${expectedCount} visit(s)`); + equal(result.title, `test visit for ${url}`, "title for search result is correct"); + } + + yield extension.startup(); + yield extension.awaitMessage("ready"); + yield PlacesTestUtils.clearHistory(); + + yield PlacesUtils.history.insertMany(PAGE_INFOS); + + extension.sendMessage("check-history"); + + let results = yield extension.awaitMessage("empty-search"); + equal(results.length, 3, "history.search with empty text returned 3 results"); + checkResult(results, SINGLE_VISIT_URL, 1); + checkResult(results, DOUBLE_VISIT_URL, 2); + checkResult(results, MOZILLA_VISIT_URL, 1); + + results = yield extension.awaitMessage("text-search"); + equal(results.length, 1, "history.search with specific text returned 1 result"); + checkResult(results, MOZILLA_VISIT_URL, 1); + + results = yield extension.awaitMessage("max-results-search"); + equal(results.length, 1, "history.search with maxResults returned 1 result"); + checkResult(results, DOUBLE_VISIT_URL, 2); + + results = yield extension.awaitMessage("date-range-search"); + equal(results.length, 2, "history.search with a date range returned 2 result"); + checkResult(results, DOUBLE_VISIT_URL, 2); + checkResult(results, SINGLE_VISIT_URL, 1); + + yield extension.awaitFinish("search"); + yield extension.unload(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_add_url() { + function background() { + const TEST_DOMAIN = "http://example.com/"; + + browser.test.onMessage.addListener((msg, testData) => { + let [details, type] = testData; + details.url = details.url || `${TEST_DOMAIN}${type}`; + if (msg === "add-url") { + details.title = `Title for ${type}`; + browser.history.addUrl(details).then(() => { + return browser.history.search({text: details.url}); + }).then(results => { + browser.test.assertEq(1, results.length, "1 result found when searching for added URL"); + browser.test.sendMessage("url-added", {details, result: results[0]}); + }); + } else if (msg === "expect-failure") { + let expectedMsg = testData[2]; + browser.history.addUrl(details).then(() => { + browser.test.fail(`Expected error thrown for ${type}`); + }, error => { + browser.test.assertTrue( + error.message.includes(expectedMsg), + `"Expected error thrown when trying to add a URL with ${type}` + ); + browser.test.sendMessage("add-failed"); + }); + } + }); + + browser.test.sendMessage("ready"); + } + + let addTestData = [ + [{}, "default"], + [{visitTime: new Date()}, "with_date"], + [{visitTime: Date.now()}, "with_ms_number"], + [{visitTime: new Date().toISOString()}, "with_iso_string"], + [{transition: "typed"}, "valid_transition"], + ]; + + let failTestData = [ + [{transition: "generated"}, "an invalid transition", "|generated| is not a supported transition for history"], + [{visitTime: Date.now() + 1000000}, "a future date", "cannot be a future date"], + [{url: "about.config"}, "an invalid url", "about.config is not a valid URL"], + ]; + + function* checkUrl(results) { + ok((yield PlacesTestUtils.isPageInDB(results.details.url)), `${results.details.url} found in history database`); + ok(PlacesUtils.isValidGuid(results.result.id), "URL was added with a valid id"); + equal(results.result.title, results.details.title, "URL was added with the correct title"); + if (results.details.visitTime) { + equal(results.result.lastVisitTime, + Number(ExtensionUtils.normalizeTime(results.details.visitTime)), + "URL was added with the correct date"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + yield PlacesTestUtils.clearHistory(); + yield extension.startup(); + yield extension.awaitMessage("ready"); + + for (let data of addTestData) { + extension.sendMessage("add-url", data); + let results = yield extension.awaitMessage("url-added"); + yield checkUrl(results); + } + + for (let data of failTestData) { + extension.sendMessage("expect-failure", data); + yield extension.awaitMessage("add-failed"); + } + + yield extension.unload(); +}); + +add_task(function* test_get_visits() { + function background() { + const TEST_DOMAIN = "http://example.com/"; + const FIRST_DATE = Date.now(); + const INITIAL_DETAILS = { + url: TEST_DOMAIN, + visitTime: FIRST_DATE, + transition: "link", + }; + + let visitIds = new Set(); + + function checkVisit(visit, expected) { + visitIds.add(visit.visitId); + browser.test.assertEq(expected.visitTime, visit.visitTime, "visit has the correct visitTime"); + browser.test.assertEq(expected.transition, visit.transition, "visit has the correct transition"); + browser.history.search({text: expected.url}).then(results => { + // all results will have the same id, so we only need to use the first one + browser.test.assertEq(results[0].id, visit.id, "visit has the correct id"); + }); + } + + let details = Object.assign({}, INITIAL_DETAILS); + + browser.history.addUrl(details).then(() => { + return browser.history.getVisits({url: details.url}); + }).then(results => { + browser.test.assertEq(1, results.length, "the expected number of visits were returned"); + checkVisit(results[0], details); + details.url = `${TEST_DOMAIN}/1/`; + return browser.history.addUrl(details); + }).then(() => { + return browser.history.getVisits({url: details.url}); + }).then(results => { + browser.test.assertEq(1, results.length, "the expected number of visits were returned"); + checkVisit(results[0], details); + details.visitTime = FIRST_DATE - 1000; + details.transition = "typed"; + return browser.history.addUrl(details); + }).then(() => { + return browser.history.getVisits({url: details.url}); + }).then(results => { + browser.test.assertEq(2, results.length, "the expected number of visits were returned"); + checkVisit(results[0], INITIAL_DETAILS); + checkVisit(results[1], details); + }).then(() => { + browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId"); + browser.test.notifyPass("get-visits"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + yield PlacesTestUtils.clearHistory(); + yield extension.startup(); + + yield extension.awaitFinish("get-visits"); + yield extension.unload(); +}); + +add_task(function* test_on_visited() { + const SINGLE_VISIT_URL = "http://example.com/1/"; + const DOUBLE_VISIT_URL = "http://example.com/2/"; + let visitDate = new Date(1999, 9, 9, 9, 9).getTime(); + + // pages/visits to add via History.insertMany + const PAGE_INFOS = [ + { + url: SINGLE_VISIT_URL, + title: `visit to ${SINGLE_VISIT_URL}`, + visits: [ + {date: new Date(visitDate)}, + ], + }, + { + url: DOUBLE_VISIT_URL, + title: `visit to ${DOUBLE_VISIT_URL}`, + visits: [ + {date: new Date(visitDate += 1000)}, + {date: new Date(visitDate += 1000)}, + ], + }, + ]; + + function background() { + let onVisitedData = []; + + browser.history.onVisited.addListener(data => { + if (data.url.includes("moz-extension")) { + return; + } + onVisitedData.push(data); + if (onVisitedData.length == 3) { + browser.test.sendMessage("on-visited-data", onVisitedData); + } + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + yield PlacesTestUtils.clearHistory(); + yield extension.startup(); + yield extension.awaitMessage("ready"); + + yield PlacesUtils.history.insertMany(PAGE_INFOS); + + let onVisitedData = yield extension.awaitMessage("on-visited-data"); + + function checkOnVisitedData(index, expected) { + let onVisited = onVisitedData[index]; + ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id"); + equal(onVisited.url, expected.url, "onVisited received the expected url"); + // Title will be blank until bug 1287928 lands + // https://bugzilla.mozilla.org/show_bug.cgi?id=1287928 + equal(onVisited.title, "", "onVisited received a blank title"); + equal(onVisited.lastVisitTime, expected.time, "onVisited received the expected time"); + equal(onVisited.visitCount, expected.visitCount, "onVisited received the expected visitCount"); + } + + let expected = { + url: PAGE_INFOS[0].url, + title: PAGE_INFOS[0].title, + time: PAGE_INFOS[0].visits[0].date.getTime(), + visitCount: 1, + }; + checkOnVisitedData(0, expected); + + expected.url = PAGE_INFOS[1].url; + expected.title = PAGE_INFOS[1].title; + expected.time = PAGE_INFOS[1].visits[0].date.getTime(); + checkOnVisitedData(1, expected); + + expected.time = PAGE_INFOS[1].visits[1].date.getTime(); + expected.visitCount = 2; + checkOnVisitedData(2, expected); + + yield extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js new file mode 100644 index 000000000..4de7afe01 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js @@ -0,0 +1,24 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + + +add_task(function* test_manifest_commands() { + let normalized = yield ExtensionTestUtils.normalizeManifest({ + "commands": { + "toggle-feature": { + "suggested_key": {"default": "Shifty+Y"}, + "description": "Send a 'toggle-feature' event to the extension", + }, + }, + }); + + let expectedError = ( + String.raw`commands.toggle-feature.suggested_key.default: Value must either: ` + + String.raw`match the pattern /^\s*(Alt|Ctrl|Command|MacCtrl)\s*\+\s*(Shift\s*\+\s*)?([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)\s*$/, or ` + + String.raw`match the pattern /^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$/` + ); + + ok(normalized.error.includes(expectedError), + `The manifest error ${JSON.stringify(normalized.error)} must contain ${JSON.stringify(expectedError)}`); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js new file mode 100644 index 000000000..2cb141235 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js @@ -0,0 +1,61 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function* testKeyword(params) { + let normalized = yield ExtensionTestUtils.normalizeManifest({ + "omnibox": { + "keyword": params.keyword, + }, + }); + + if (params.expectError) { + let expectedError = ( + String.raw`omnibox.keyword: String "${params.keyword}" ` + + String.raw`must match /^[^?\s:]([^\s:]*[^/\s:])?$/` + ); + ok(normalized.error.includes(expectedError), + `The manifest error ${JSON.stringify(normalized.error)} ` + + `must contain ${JSON.stringify(expectedError)}`); + } else { + equal(normalized.error, undefined, "Should not have an error"); + equal(normalized.errors.length, 0, "Should not have warnings"); + } +} + +add_task(function* test_manifest_commands() { + // accepted single character keywords + yield testKeyword({keyword: "a", expectError: false}); + yield testKeyword({keyword: "-", expectError: false}); + yield testKeyword({keyword: "嗨", expectError: false}); + yield testKeyword({keyword: "*", expectError: false}); + yield testKeyword({keyword: "/", expectError: false}); + + // rejected single character keywords + yield testKeyword({keyword: "?", expectError: true}); + yield testKeyword({keyword: " ", expectError: true}); + yield testKeyword({keyword: ":", expectError: true}); + + // accepted multi-character keywords + yield testKeyword({keyword: "aa", expectError: false}); + yield testKeyword({keyword: "http", expectError: false}); + yield testKeyword({keyword: "f?a", expectError: false}); + yield testKeyword({keyword: "fa?", expectError: false}); + yield testKeyword({keyword: "f/x", expectError: false}); + yield testKeyword({keyword: "/fx", expectError: false}); + + // rejected multi-character keywords + yield testKeyword({keyword: " a", expectError: true}); + yield testKeyword({keyword: "a ", expectError: true}); + yield testKeyword({keyword: " ", expectError: true}); + yield testKeyword({keyword: " a ", expectError: true}); + yield testKeyword({keyword: "?fx", expectError: true}); + yield testKeyword({keyword: "fx/", expectError: true}); + yield testKeyword({keyword: "f:x", expectError: true}); + yield testKeyword({keyword: "fx:", expectError: true}); + yield testKeyword({keyword: "f x", expectError: true}); + + // miscellaneous tests + yield testKeyword({keyword: "こんにちは", expectError: false}); + yield testKeyword({keyword: "http://", expectError: true}); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js new file mode 100644 index 000000000..2c436535d --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js @@ -0,0 +1,57 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals chrome */ + +function* testPermission(options) { + function background(bgOptions) { + browser.test.sendMessage("typeof-namespace", { + browser: typeof browser[bgOptions.namespace], + chrome: typeof chrome[bgOptions.namespace], + }); + } + + let extensionDetails = { + background: `(${background})(${JSON.stringify(options)})`, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionDetails); + + yield extension.startup(); + + let types = yield extension.awaitMessage("typeof-namespace"); + equal(types.browser, "undefined", `Type of browser.${options.namespace} without manifest entry`); + equal(types.chrome, "undefined", `Type of chrome.${options.namespace} without manifest entry`); + + yield extension.unload(); + + extensionDetails.manifest = options.manifest; + extension = ExtensionTestUtils.loadExtension(extensionDetails); + + yield extension.startup(); + + types = yield extension.awaitMessage("typeof-namespace"); + equal(types.browser, "object", `Type of browser.${options.namespace} with manifest entry`); + equal(types.chrome, "object", `Type of chrome.${options.namespace} with manifest entry`); + + yield extension.unload(); +} + +add_task(function* test_browserAction() { + yield testPermission({ + namespace: "browserAction", + manifest: { + browser_action: {}, + }, + }); +}); + +add_task(function* test_pageAction() { + yield testPermission({ + namespace: "pageAction", + manifest: { + page_action: {}, + }, + }); +}); diff --git a/browser/components/extensions/test/xpcshell/xpcshell.ini b/browser/components/extensions/test/xpcshell/xpcshell.ini new file mode 100644 index 000000000..b9148a697 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/xpcshell.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = head.js +tail = +firefox-appdir = browser +tags = webextensions + +[test_ext_bookmarks.js] +[test_ext_history.js] +[test_ext_manifest_commands.js] +[test_ext_manifest_omnibox.js] +[test_ext_manifest_permissions.js] |