summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /browser/components/extensions/test
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'browser/components/extensions/test')
-rw-r--r--browser/components/extensions/test/browser/.eslintrc.js36
-rw-r--r--browser/components/extensions/test/browser/browser.ini115
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_context.js398
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js68
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js321
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js210
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_popup.js413
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js304
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_simple.js59
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js113
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js133
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_getAll.js81
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_onCommand.js229
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contentscript_connect.js67
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus.js342
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_checkboxes.js96
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js76
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js196
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_radioGroups.js100
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_uninstall.js84
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js254
-rw-r--r--browser/components/extensions/test/browser/browser_ext_currentWindow.js149
-rw-r--r--browser/components/extensions/test/browser/browser_ext_getViews.js198
-rw-r--r--browser/components/extensions/test/browser/browser_ext_incognito_popup.js108
-rw-r--r--browser/components/extensions/test/browser/browser_ext_incognito_views.js121
-rw-r--r--browser/components/extensions/test/browser/browser_ext_lastError.js55
-rw-r--r--browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js173
-rw-r--r--browser/components/extensions/test/browser/browser_ext_omnibox.js286
-rw-r--r--browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js66
-rw-r--r--browser/components/extensions/test/browser/browser_ext_pageAction_context.js178
-rw-r--r--browser/components/extensions/test/browser/browser_ext_pageAction_popup.js238
-rw-r--r--browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js169
-rw-r--r--browser/components/extensions/test/browser/browser_ext_pageAction_simple.js60
-rw-r--r--browser/components/extensions/test/browser/browser_ext_pageAction_title.js226
-rw-r--r--browser/components/extensions/test/browser/browser_ext_popup_api_injection.js101
-rw-r--r--browser/components/extensions/test/browser/browser_ext_popup_background.js133
-rw-r--r--browser/components/extensions/test/browser/browser_ext_popup_corners.js98
-rw-r--r--browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js93
-rw-r--r--browser/components/extensions/test/browser/browser_ext_popup_shutdown.js77
-rw-r--r--browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js276
-rw-r--r--browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js101
-rw-r--r--browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js94
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js97
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js61
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js96
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sessions_restore.js134
-rw-r--r--browser/components/extensions/test/browser/browser_ext_simple.js57
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js74
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_audio.js203
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js155
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js156
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_create.js166
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js66
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js47
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js146
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_events.js280
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js234
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js217
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js189
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js67
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js107
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js70
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js86
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_move.js103
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_move_window.js98
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js43
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js42
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js126
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js198
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_query.js224
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_reload.js54
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js58
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js95
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js227
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_update.js45
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_update_url.js110
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_zoom.js222
-rw-r--r--browser/components/extensions/test/browser/browser_ext_topwindowid.js23
-rw-r--r--browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js45
-rw-r--r--browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js168
-rw-r--r--browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js251
-rw-r--r--browser/components/extensions/test/browser/browser_ext_webRequest.js95
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows.js33
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js61
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_create.js142
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_create_params.js33
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js140
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_create_url.js84
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_events.js115
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_size.js114
-rw-r--r--browser/components/extensions/test/browser/browser_ext_windows_update.js189
-rw-r--r--browser/components/extensions/test/browser/context.html23
-rw-r--r--browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html19
-rw-r--r--browser/components/extensions/test/browser/context_tabs_onUpdated_page.html18
-rw-r--r--browser/components/extensions/test/browser/ctxmenu-image.pngbin0 -> 5401 bytes
-rw-r--r--browser/components/extensions/test/browser/file_bypass_cache.sjs11
-rw-r--r--browser/components/extensions/test/browser/file_dummy.html9
-rw-r--r--browser/components/extensions/test/browser/file_iframe_document.html10
-rw-r--r--browser/components/extensions/test/browser/file_iframe_document.sjs41
-rw-r--r--browser/components/extensions/test/browser/file_language_fr_en.html14
-rw-r--r--browser/components/extensions/test/browser/file_language_ja.html10
-rw-r--r--browser/components/extensions/test/browser/file_language_tlh.html12
-rw-r--r--browser/components/extensions/test/browser/file_popup_api_injection_a.html10
-rw-r--r--browser/components/extensions/test/browser/file_popup_api_injection_b.html10
-rw-r--r--browser/components/extensions/test/browser/head.js263
-rw-r--r--browser/components/extensions/test/browser/head_pageAction.js157
-rw-r--r--browser/components/extensions/test/browser/head_sessions.js47
-rw-r--r--browser/components/extensions/test/browser/searchSuggestionEngine.sjs9
-rw-r--r--browser/components/extensions/test/browser/searchSuggestionEngine.xml9
-rw-r--r--browser/components/extensions/test/mochitest/mochitest.ini6
-rw-r--r--browser/components/extensions/test/mochitest/test_ext_all_apis.html75
-rw-r--r--browser/components/extensions/test/xpcshell/.eslintrc.js9
-rw-r--r--browser/components/extensions/test/xpcshell/head.js55
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_bookmarks.js601
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_history.js487
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js24
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js61
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js57
-rw-r--r--browser/components/extensions/test/xpcshell/xpcshell.ini11
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
new file mode 100644
index 000000000..4c3be5084
--- /dev/null
+++ b/browser/components/extensions/test/browser/ctxmenu-image.png
Binary files differ
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]