summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test/browser')
-rw-r--r--toolkit/mozapps/extensions/test/browser/.eslintrc.js14
-rw-r--r--toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml6
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsabin0 -> 4210 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsabin0 -> 4210 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsabin0 -> 4218 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json13
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsabin0 -> 4213 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json22
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsabin0 -> 4197 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf4
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json11
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html9
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser.toml193
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js220
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js129
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js204
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js85
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug572561.js96
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js36
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js265
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_dragdrop.js270
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js122
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js176
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js406
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_history_navigation.js623
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js1093
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js185
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js827
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js1675
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js668
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js219
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js83
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_list_view.js1063
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js293
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js185
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js651
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js136
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js311
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js180
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js165
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js229
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js178
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_updates.js750
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js290
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_installssl.js378
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js362
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_local_install.js245
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js331
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js198
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js180
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js93
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js15
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js128
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js124
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js178
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_reinstall.js277
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js262
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js166
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js214
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js76
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_subframe_install.js234
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_task_next_test.js17
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updateid.js87
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.js389
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.json17
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^1
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js62
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi.js125
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js375
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_access.js146
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js124
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js63
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_install.js652
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js60
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js79
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js72
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webext_icon.js82
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js593
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery/api_response.json679
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json1
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery/small-1x1.pngbin0 -> 82 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/head.js1714
-rw-r--r--toolkit/mozapps/extensions/test/browser/head_abuse_report.js615
-rw-r--r--toolkit/mozapps/extensions/test/browser/head_disco.js125
-rw-r--r--toolkit/mozapps/extensions/test/browser/moz.build31
-rw-r--r--toolkit/mozapps/extensions/test/browser/redirect.sjs5
-rw-r--r--toolkit/mozapps/extensions/test/browser/sandboxed.html11
-rw-r--r--toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^1
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html30
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html13
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml6
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkframed.html7
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html29
105 files changed, 21857 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/test/browser/.eslintrc.js b/toolkit/mozapps/extensions/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..f2b9e072f9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/.eslintrc.js
@@ -0,0 +1,14 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+
+ rules: {
+ "no-unused-vars": [
+ "error",
+ { args: "none", varsIgnorePattern: "^end_test$" },
+ ],
+ },
+};
diff --git a/toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml b/toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml
new file mode 100644
index 0000000000..e8cde29666
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="addon-test-pref-window">
+ <label value="Oh hai!"/>
+</window>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf
new file mode 100644
index 0000000000..725ac8016f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf
@@ -0,0 +1,8 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: MD5 SHA1 SHA256
+MD5-Digest: mCLu38qfGN3trj7qKQQeEA==
+SHA1-Digest: A1BaJErQY6KqnYDijP0lglrehk0=
+SHA256-Digest: p2vjGP7DRqrK81NfT4LqnF7a5p8+lEuout5WLBhk9AA=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..046a0285c7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf
new file mode 100644
index 0000000000..ad4e81b574
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf
@@ -0,0 +1,5 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: LrrwWBKNYWeVd205Hq+JwQ==
+SHA1-Digest-Manifest: MeqqQN+uuf0MVesMXxbBtYN+5tU=
+SHA256-Digest-Manifest: iWCxfAJX593Cn4l8R63jaQETO5HX3XOhcnpQ7nMiPlg=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json
new file mode 100644
index 0000000000..91012f24ed
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "dragdrop-1@tests.mozilla.org"
+ }
+ },
+
+ "name": "Drag Drop test 1",
+ "version": "1.0"
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf
new file mode 100644
index 0000000000..1da3c41b23
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf
@@ -0,0 +1,8 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: MD5 SHA1 SHA256
+MD5-Digest: 3dL7JFDBPC63pSFI5x+Z7Q==
+SHA1-Digest: l1cKPyWJIYdZyvumH9VfJ6fpqVA=
+SHA256-Digest: QHTjPqTMXxt5tl8zOaAzpQ8FZLqZx8LRF9LmzY+RCDQ=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..170a361620
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf
new file mode 100644
index 0000000000..5301e431f7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf
@@ -0,0 +1,5 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: c30hzcI1ISlt46ODjVVJ2w==
+SHA1-Digest-Manifest: 2yMpQHuLM0J61T7vt11NHoYI1tU=
+SHA256-Digest-Manifest: qtsYxiv1zGWBp7JWxLWrIztIdxIt+i3CToReEx5fkyw=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json
new file mode 100644
index 0000000000..958aa03649
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "dragdrop-2@tests.mozilla.org"
+ }
+ },
+
+ "name": "Drag Drop test 2",
+ "version": "1.1"
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf
new file mode 100644
index 0000000000..e508bcd22f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf
@@ -0,0 +1,8 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: MD5 SHA1 SHA256
+MD5-Digest: Wzo/k6fhArpFb4UB2hIKlg==
+SHA1-Digest: D/WDy9api0X7OgRM6Gkvfbyzogo=
+SHA256-Digest: IWBdbytHgPLtCMKKhiZ3jenxKmKiRAhh3ce8iP5AVWU=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..a026680e91
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf
new file mode 100644
index 0000000000..16a1461f37
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf
@@ -0,0 +1,5 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: ovtNeIie34gMM5l18zP2MA==
+SHA1-Digest-Manifest: c5owdrvcOINxKp/HprYkWXXI/js=
+SHA256-Digest-Manifest: uLPmoONlxFYxWeSTOEPJ9hN2yMDDZMJL1PoNIWcqKG4=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json
new file mode 100644
index 0000000000..b204e1bca7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json
@@ -0,0 +1,13 @@
+{
+ "manifest_version": 2,
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "dragdrop-incompat@tests.mozilla.org",
+ "strict_max_version": "45.0"
+ }
+ },
+
+ "name": "Incomatible Drag Drop test",
+ "version": "1.1"
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf
new file mode 100644
index 0000000000..eea5cbd501
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf
@@ -0,0 +1,8 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: MD5 SHA1 SHA256
+MD5-Digest: b4Q2C4GsIJfRLsXc7T2ldQ==
+SHA1-Digest: UG5rHxpzKmdlGrquXaguiAGDu8E=
+SHA256-Digest: WZrN9SdGBux9t3lV7TVIvyUG/L1px4er2dU3TsBpC4s=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..68621e19be
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf
new file mode 100644
index 0000000000..fe6baa8dac
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf
@@ -0,0 +1,5 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: zqRm8+jxS0iRUGWeArGkXg==
+SHA1-Digest-Manifest: pa/31Ll1PYx0dPBQ6C+fd1/wJO4=
+SHA256-Digest-Manifest: DJELIyswfwgeL0kaRqogXW2bzUKhn+Pickfv6WHBsW8=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json
new file mode 100644
index 0000000000..adc0ae09ee
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "sslinstall-1@tests.mozilla.org"
+ }
+ },
+
+ "name": "SSL Install Tests",
+ "version": "1.0"
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json
new file mode 100644
index 0000000000..7a399ddc17
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json
@@ -0,0 +1,22 @@
+{
+ "manifest_version": 2,
+
+ "name": "Theme test",
+ "version": "1.0",
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "theme@tests.mozilla.org"
+ }
+ },
+
+ "theme": {
+ "images": {
+ "theme_frame": "testImage.png"
+ },
+ "colors": {
+ "frame": "#000000",
+ "tab_background_text": "#ffffff"
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf
new file mode 100644
index 0000000000..a8c72c4794
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf
@@ -0,0 +1,12 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: MD5 SHA1
+MD5-Digest: Rnoaa6yWePDor5y5/SLFaw==
+SHA1-Digest: k51DtKj7bYrwkFJDdmYNDQeUBlA=
+
+Name: options.html
+Digest-Algorithms: MD5 SHA1
+MD5-Digest: vTjxWlRpioEhTZGKTNUqIw==
+SHA1-Digest: Y/mr6A34LsvekgRpdhyZRwPF1Vw=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..8b6320adda
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf
new file mode 100644
index 0000000000..ba5fd22caa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf
@@ -0,0 +1,4 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: rdmx8VMNzkZ5tRf7tt8G1w==
+SHA1-Digest-Manifest: gjtTe8X9Tg46Hz2h4Tru3T02hmE=
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json
new file mode 100644
index 0000000000..e808cd5ab6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json
@@ -0,0 +1,11 @@
+{
+ "manifest_version": 2,
+
+ "name": "Test options_ui",
+ "description": "Test add-ons manager handling options_ui with no id in manifest.json",
+ "version": "1.2",
+
+ "options_ui": {
+ "page": "options.html"
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html
new file mode 100644
index 0000000000..ea804601b5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ </head>
+ <body>
+ <div id="options-test-panel" />
+ </body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/browser.toml b/toolkit/mozapps/extensions/test/browser/browser.toml
new file mode 100644
index 0000000000..1daf6211f8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser.toml
@@ -0,0 +1,193 @@
+[DEFAULT]
+tags = "addons"
+support-files = [
+ "addons/browser_dragdrop1.xpi",
+ "addons/browser_dragdrop1.zip",
+ "addons/browser_dragdrop2.xpi",
+ "addons/browser_dragdrop2.zip",
+ "addons/browser_dragdrop_incompat.xpi",
+ "addons/browser_installssl.xpi",
+ "addons/browser_theme.xpi",
+ "addons/options_signed.xpi",
+ "addons/options_signed/*",
+ "addon_prefs.xhtml",
+ "discovery/api_response.json",
+ "discovery/api_response_empty.json",
+ "discovery/small-1x1.png",
+ "head.js",
+ "redirect.sjs",
+ "browser_updatessl.json",
+ "browser_updatessl.json^headers^",
+ "sandboxed.html",
+ "sandboxed.html^headers^",
+ "webapi_addon_listener.html",
+ "webapi_checkavailable.html",
+ "webapi_checkchromeframe.xhtml",
+ "webapi_checkframed.html",
+ "webapi_checknavigatedwindow.html",
+ "!/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi",
+ "!/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi",
+ "!/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html",
+ "!/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi",
+ "!/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi",
+]
+
+generated-files = [
+ "addons/browser_dragdrop1.xpi",
+ "addons/browser_dragdrop1.zip",
+ "addons/browser_dragdrop2.xpi",
+ "addons/browser_dragdrop2.zip",
+ "addons/browser_dragdrop_incompat.xpi",
+ "addons/browser_installssl.xpi",
+ "addons/browser_theme.xpi",
+ "addons/options_signed.xpi",
+]
+
+skip-if = [
+ "os == 'linux' && asan", # Bug 1713895 - new Fission platform triage
+]
+prefs = [
+ "dom.webmidi.enabled=true",
+ "midi.testing=true",
+]
+
+["browser_AMBrowserExtensionsImport.js"]
+
+["browser_about_debugging_link.js"]
+
+["browser_addon_list_reordering.js"]
+
+["browser_amo_abuse_report.js"]
+
+["browser_bug572561.js"]
+
+["browser_checkAddonCompatibility.js"]
+
+["browser_colorwaybuiltins_migration.js"]
+skip-if = [
+ "app-name != 'firefox'",
+]
+
+["browser_dragdrop.js"]
+skip-if = ["true"] # Bug 1626824
+
+["browser_file_xpi_no_process_switch.js"]
+
+["browser_globalwarnings.js"]
+
+["browser_gmpProvider.js"]
+
+["browser_history_navigation.js"]
+https_first_disabled = true
+
+["browser_html_abuse_report.js"]
+support-files = ["head_abuse_report.js"]
+
+["browser_html_abuse_report_dialog.js"]
+support-files = ["head_abuse_report.js"]
+
+["browser_html_detail_permissions.js"]
+
+["browser_html_detail_view.js"]
+
+["browser_html_discover_view.js"]
+https_first_disabled = true
+support-files = ["head_disco.js"]
+
+["browser_html_discover_view_clientid.js"]
+
+["browser_html_discover_view_prefs.js"]
+
+["browser_html_list_view.js"]
+
+["browser_html_list_view_recommendations.js"]
+skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results for .popup-notification-primary-button.primary.footer-button
+
+["browser_html_message_bar.js"]
+
+["browser_html_options_ui.js"]
+
+["browser_html_options_ui_in_tab.js"]
+
+["browser_html_pending_updates.js"]
+
+["browser_html_recent_updates.js"]
+
+["browser_html_recommendations.js"]
+https_first_disabled = true
+
+["browser_html_scroll_restoration.js"]
+skip-if = ["os == 'mac' && verify && debug"] # Bug 1850159
+
+["browser_html_sitepermission_addons.js"]
+
+["browser_html_updates.js"]
+https_first_disabled = true
+
+["browser_html_warning_messages.js"]
+
+["browser_installssl.js"]
+
+["browser_installtrigger_install.js"]
+
+["browser_local_install.js"]
+
+["browser_manage_shortcuts.js"]
+
+["browser_manage_shortcuts_hidden.js"]
+
+["browser_manage_shortcuts_remove.js"]
+
+["browser_menu_button_accessibility.js"]
+
+["browser_page_accessibility.js"]
+
+["browser_page_options_install_addon.js"]
+
+["browser_page_options_updates.js"]
+
+["browser_permission_prompt.js"]
+
+["browser_reinstall.js"]
+
+["browser_shortcuts_duplicate_check.js"]
+
+["browser_sidebar_categories.js"]
+
+["browser_sidebar_hidden_categories.js"]
+
+["browser_sidebar_restore_category.js"]
+
+["browser_subframe_install.js"]
+
+["browser_task_next_test.js"]
+
+["browser_updateid.js"]
+
+["browser_updatessl.js"]
+
+["browser_verify_l10n_strings.js"]
+
+["browser_webapi.js"]
+
+["browser_webapi_abuse_report.js"]
+support-files = ["head_abuse_report.js"]
+
+["browser_webapi_access.js"]
+https_first_disabled = true
+
+["browser_webapi_addon_listener.js"]
+
+["browser_webapi_enable.js"]
+
+["browser_webapi_install.js"]
+
+["browser_webapi_install_disabled.js"]
+
+["browser_webapi_theme.js"]
+
+["browser_webapi_uninstall.js"]
+
+["browser_webext_icon.js"]
+
+["browser_webext_incognito.js"]
diff --git a/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js b/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js
new file mode 100644
index 0000000000..654e3cd91e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { AMBrowserExtensionsImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+// This test verifies the global notification in `about:addons` when there are
+// pending imported add-ons. The appmenu UI is covered by tests in:
+// `browser/components/extensions/test/browser/browser_AMBrowserExtensionsImport.js`.
+
+AddonTestUtils.initMochitest(this);
+
+const TEST_SERVER = AddonTestUtils.createHttpServer();
+
+const ADDONS = {
+ ext1: {
+ manifest: {
+ name: "Ext 1",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "ff@ext-1" } },
+ permissions: ["history"],
+ },
+ },
+ ext2: {
+ manifest: {
+ name: "Ext 2",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "ff@ext-2" } },
+ permissions: ["history"],
+ },
+ },
+};
+// Populated in `setup()`.
+const XPIS = {};
+// Populated in `setup()`.
+const ADDON_SEARCH_RESULTS = {};
+
+const mockAddonRepository = ({ addons = [] }) => {
+ return {
+ async getMappedAddons(browserID, extensionIDs) {
+ return Promise.resolve({
+ addons,
+ matchedIDs: [],
+ unmatchedIDs: [],
+ });
+ },
+ };
+};
+
+const assertWarningShown = async (
+ win,
+ stack,
+ expectedWarningType = "imported-addons",
+ expectAction = true
+) => {
+ Assert.equal(stack.childElementCount, 1, "expected a global warning");
+ const messageBar = stack.firstElementChild;
+ Assert.equal(
+ messageBar.getAttribute("warning-type"),
+ expectedWarningType,
+ `expected a warning for ${expectedWarningType}`
+ );
+ Assert.equal(
+ messageBar.getAttribute("data-l10n-id"),
+ `extensions-warning-${expectedWarningType}2`,
+ "expected correct l10n ID"
+ );
+ await win.document.l10n.translateElements([messageBar]);
+
+ if (expectAction) {
+ const button = messageBar.querySelector("button");
+ Assert.equal(
+ button.getAttribute("action"),
+ expectedWarningType,
+ `expected a button for ${expectedWarningType}`
+ );
+ Assert.equal(
+ button.getAttribute("data-l10n-id"),
+ `extensions-warning-${expectedWarningType}-button`,
+ "expected correct l10n ID on the button"
+ );
+ await win.document.l10n.translateElements([button]);
+ }
+};
+
+add_setup(async function setup() {
+ for (const [name, data] of Object.entries(ADDONS)) {
+ XPIS[name] = AddonTestUtils.createTempWebExtensionFile(data);
+ TEST_SERVER.registerFile(`/addons/${name}.xpi`, XPIS[name]);
+
+ ADDON_SEARCH_RESULTS[name] = {
+ id: data.manifest.browser_specific_settings.gecko.id,
+ name: data.name,
+ version: data.version,
+ sourceURI: Services.io.newURI(
+ `http://localhost:${TEST_SERVER.identity.primaryPort}/addons/${name}.xpi`
+ ),
+ icons: {},
+ };
+ }
+
+ registerCleanupFunction(() => {
+ // Clear the add-on repository override.
+ AMBrowserExtensionsImport._addonRepository = null;
+ });
+});
+
+add_task(async function test_aboutaddons_global_message() {
+ const browserID = "some-browser-id";
+ const extensionIDs = ["ext-1", "ext-2"];
+ AMBrowserExtensionsImport._addonRepository = mockAddonRepository({
+ addons: Object.values(ADDON_SEARCH_RESULTS),
+ });
+
+ // Global warnings should be displayed in all the `about:addons` views but
+ // the migration wizard links to the default view. That's why we load this
+ // view here, too (as opposed to, e.g., `"extensions"`).
+ const win = await loadInitialView();
+ const stack = win.document.querySelector("global-warnings");
+
+ Assert.equal(stack.childElementCount, 0, "expected no global warning");
+
+ let promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ // Start a first import...
+ await AMBrowserExtensionsImport.stageInstalls(browserID, extensionIDs);
+ await promiseTopic;
+ // We expect a warning about the imported add-ons to be shown.
+ await assertWarningShown(win, stack);
+
+ // ...then cancel it.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-cancelled"
+ );
+ await AMBrowserExtensionsImport.cancelInstalls();
+ await promiseTopic;
+
+ // At this point, the warning about the imported add-ons should be hidden.
+ Assert.equal(stack.childElementCount, 0, "expected no global warning");
+
+ // We start a second import here, then we make sure an imported-addons
+ // messagebar doesn't prevent the other global warning types to be shown.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ const result = await AMBrowserExtensionsImport.stageInstalls(
+ browserID,
+ extensionIDs
+ );
+ await promiseTopic;
+ await assertWarningShown(win, stack);
+
+ info("Verify safe-mode is not hidden by an imported-addons messagebar");
+ stack.inSafeMode = true;
+ stack.refresh();
+ await assertWarningShown(
+ win,
+ stack,
+ "safe-mode",
+ false /* no button expected */
+ );
+ stack.inSafeMode = false;
+
+ info(
+ "Verify check-compatibility is not hidden by an imported-addons messagebar"
+ );
+ AddonManager.checkCompatibility = false;
+ stack.refresh();
+ await assertWarningShown(win, stack, "check-compatibility");
+ AddonManager.checkCompatibility = true;
+
+ info("Verify update-security is not hidden by an imported-addons messagebar");
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.checkUpdateSecurity", false]],
+ });
+ stack.refresh();
+ await assertWarningShown(win, stack, "update-security");
+ await SpecialPowers.popPrefEnv();
+
+ // After making sure the imported-addons messagebar is visible again, we
+ // finally complete the pending import with the UI from the global warning.
+ info(
+ "Verify pending imported addons can be completed from the messagebar action"
+ );
+ stack.refresh();
+ await assertWarningShown(win, stack, "imported-addons");
+
+ // Complete the installation of the add-ons by clicking on the button in the
+ // global warning.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-complete"
+ );
+ const endedPromises = result.importedAddonIDs.map(id =>
+ AddonTestUtils.promiseInstallEvent(
+ "onInstallEnded",
+ install => install.addon.id === id
+ )
+ );
+ stack.firstElementChild.querySelector("button").click();
+ await Promise.all([...endedPromises, promiseTopic]);
+
+ // At this point, the warning about the imported add-ons should be hidden
+ // because the add-ons are installed.
+ Assert.equal(stack.childElementCount, 0, "expected no global warning");
+
+ for (const id of result.importedAddonIDs) {
+ const addon = await AddonManager.getAddonByID(id);
+ Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`);
+ await addon.uninstall();
+ }
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js b/toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js
new file mode 100644
index 0000000000..c7351f054c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+// Allow rejections related to closing an about:debugging too soon after it has been
+// just opened in a new tab and loaded.
+PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/);
+
+function waitForDispatch(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ type: "@@service/waitUntil",
+ predicate: action => action.type === type,
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ },
+ });
+ });
+}
+
+/**
+ * Wait for all client requests to settle, meaning here that no new request has been
+ * dispatched after the provided delay. (NOTE: same test helper used in about:debugging tests)
+ */
+async function waitForRequestsToSettle(store, delay = 500) {
+ let hasSettled = false;
+
+ // After each iteration of this while loop, we check is the timerPromise had the time
+ // to resolve or if we captured a REQUEST_*_SUCCESS action before.
+ while (!hasSettled) {
+ let timer;
+
+ // This timer will be executed only if no REQUEST_*_SUCCESS action is dispatched
+ // during the delay. We consider that when no request are received for some time, it
+ // means there are no ongoing requests anymore.
+ const timerPromise = new Promise(resolve => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ timer = setTimeout(() => {
+ hasSettled = true;
+ resolve();
+ }, delay);
+ });
+
+ // Wait either for a REQUEST_*_SUCCESS to be dispatched, or for the timer to resolve.
+ await Promise.race([
+ waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_TABS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"),
+ timerPromise,
+ ]);
+
+ // Clear the timer to avoid setting hasSettled to true accidently unless timerPromise
+ // was the first to resolve.
+ clearTimeout(timer);
+ }
+}
+
+function waitForRequestsSuccess(store) {
+ return Promise.all([
+ waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_TABS_SUCCESS"),
+ waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"),
+ ]);
+}
+
+add_task(async function testAboutDebugging() {
+ let win = await loadInitialView("extension");
+
+ let aboutAddonsTab = gBrowser.selectedTab;
+ let debugAddonsBtn = win.document.querySelector(
+ '#page-options [action="debug-addons"]'
+ );
+
+ // Verify the about:debugging is loaded.
+ info(`Check about:debugging loads`);
+ let loaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:debugging#/runtime/this-firefox",
+ true
+ );
+ debugAddonsBtn.click();
+ await loaded;
+ let aboutDebuggingTab = gBrowser.selectedTab;
+ const { AboutDebugging } = aboutDebuggingTab.linkedBrowser.contentWindow;
+ // Avoid test failures due to closing the about:debugging tab
+ // while it is still initializing.
+ info("Wait until about:debugging actions are finished");
+ await waitForRequestsSuccess(AboutDebugging.store);
+
+ info("Switch back to about:addons");
+ await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab);
+ is(gBrowser.selectedTab, aboutAddonsTab, "Back to about:addons");
+
+ info("Re-open about:debugging");
+ let switched = TestUtils.waitForCondition(
+ () => gBrowser.selectedTab == aboutDebuggingTab
+ );
+ debugAddonsBtn.click();
+ await switched;
+ await waitForRequestsToSettle(AboutDebugging.store);
+
+ info("Force about:debugging to a different hash URL");
+ aboutDebuggingTab.linkedBrowser.contentWindow.location.hash = "/setup";
+
+ info("Switch back to about:addons again");
+ await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab);
+ is(gBrowser.selectedTab, aboutAddonsTab, "Back to about:addons");
+
+ info("Re-open about:debugging a second time");
+ switched = TestUtils.waitForCondition(
+ () => gBrowser.selectedTab == aboutDebuggingTab
+ );
+ debugAddonsBtn.click();
+ await switched;
+
+ info("Wait until any new about:debugging request did settle");
+ // Avoid test failures due to closing the about:debugging tab
+ // while it is still initializing.
+ await waitForRequestsToSettle(AboutDebugging.store);
+
+ info("Remove the about:debugging tab");
+ BrowserTestUtils.removeTab(aboutDebuggingTab);
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js b/toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js
new file mode 100644
index 0000000000..a80a57bb7e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+function assertInSection(card, sectionName, msg) {
+ let section = card.closest("section");
+ let heading = section.querySelector(".list-section-heading");
+ is(
+ card.ownerDocument.l10n.getAttributes(heading).id,
+ `extension-${sectionName}-heading`,
+ msg
+ );
+}
+
+function waitForAnimationFrame(win) {
+ return new Promise(resolve => win.requestAnimationFrame(resolve));
+}
+
+async function clickEnableToggle(card) {
+ let isDisabled = card.addon.userDisabled;
+ let addonEvent = isDisabled ? "onEnabled" : "onDisabled";
+ let addonStateChanged = AddonTestUtils.promiseAddonEvent(addonEvent);
+ let win = card.ownerGlobal;
+ let button = card.querySelector(".extension-enable-button");
+
+ // Centre the button since "start" could be behind the sticky header.
+ button.scrollIntoView({ block: "center" });
+ EventUtils.synthesizeMouseAtCenter(button, { type: "mousemove" }, win);
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+
+ await addonStateChanged;
+ await waitForAnimationFrame(win);
+}
+
+function mouseOver(el) {
+ let win = el.ownerGlobal;
+ el.scrollIntoView({ block: "center" });
+ EventUtils.synthesizeMouseAtCenter(el, { type: "mousemove" }, win);
+ return waitForAnimationFrame(win);
+}
+
+function mouseOutOfList(win) {
+ return mouseOver(win.document.querySelector(".header-name"));
+}
+
+function pressKey(win, key) {
+ EventUtils.synthesizeKey(key, {}, win);
+ return waitForAnimationFrame(win);
+}
+
+function waitForTransitionEnd(...els) {
+ return Promise.all(
+ els.map(el =>
+ BrowserTestUtils.waitForEvent(el, "transitionend", false, e => {
+ let cardEl = el.firstElementChild;
+ return e.target == cardEl && e.propertyName == "transform";
+ })
+ )
+ );
+}
+
+add_setup(async function () {
+ // Ensure prefers-reduced-motion isn't set. Some linux environments will have
+ // this enabled by default.
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 0]],
+ });
+});
+
+add_task(async function testReordering() {
+ let addonIds = [
+ "one@mochi.test",
+ "two@mochi.test",
+ "three@mochi.test",
+ "four@mochi.test",
+ "five@mochi.test",
+ ];
+ let extensions = addonIds.map(id =>
+ ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: id,
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ })
+ );
+
+ await Promise.all(extensions.map(ext => ext.startup()));
+
+ let win = await loadInitialView("extension", { withAnimations: true });
+
+ let cardOne = getAddonCard(win, "one@mochi.test");
+ ok(!cardOne.addon.userDisabled, "extension one is enabled");
+ assertInSection(cardOne, "enabled", "cardOne is initially in Enabled");
+
+ await clickEnableToggle(cardOne);
+
+ ok(cardOne.addon.userDisabled, "extension one is now disabled");
+ assertInSection(cardOne, "enabled", "cardOne is still in Enabled");
+
+ let cardThree = getAddonCard(win, "three@mochi.test");
+ ok(!cardThree.addon.userDisabled, "extension three is enabled");
+ assertInSection(cardThree, "enabled", "cardThree is initially in Enabled");
+
+ await clickEnableToggle(cardThree);
+
+ ok(cardThree.addon.userDisabled, "extension three is now disabled");
+ assertInSection(cardThree, "enabled", "cardThree is still in Enabled");
+
+ let transitionsEnded = waitForTransitionEnd(cardOne, cardThree);
+ await mouseOutOfList(win);
+ await transitionsEnded;
+
+ assertInSection(cardOne, "disabled", "cardOne has moved to disabled");
+ assertInSection(cardThree, "disabled", "cardThree has moved to disabled");
+
+ await clickEnableToggle(cardThree);
+ await clickEnableToggle(cardOne);
+
+ assertInSection(cardOne, "disabled", "cardOne is still in disabled");
+ assertInSection(cardThree, "disabled", "cardThree is still in disabled");
+
+ info("Opening a more options menu");
+ let panel = cardThree.querySelector("panel-list");
+ EventUtils.synthesizeMouseAtCenter(
+ cardThree.querySelector('[action="more-options"]'),
+ {},
+ win
+ );
+
+ await BrowserTestUtils.waitForEvent(panel, "shown");
+ await mouseOutOfList(win);
+
+ assertInSection(cardOne, "disabled", "cardOne stays in disabled, menu open");
+ assertInSection(cardThree, "disabled", "cardThree stays in disabled");
+
+ transitionsEnded = waitForTransitionEnd(cardOne, cardThree);
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to clear the focused
+ // state with a mouse which can be done by assistive technology and keyboard
+ // by pressing `Esc` key, this rule check shall be ignored by a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ // Click outside the list to clear any focus.
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.querySelector(".header-name"),
+ {},
+ win
+ );
+ AccessibilityUtils.resetEnv();
+ await transitionsEnded;
+
+ assertInSection(cardOne, "enabled", "cardOne is now in enabled");
+ assertInSection(cardThree, "enabled", "cardThree is now in enabled");
+
+ let cardOneToggle = cardOne.querySelector(".extension-enable-button");
+ cardOneToggle.scrollIntoView({ block: "center" });
+ cardOneToggle.focus();
+ await pressKey(win, " ");
+ await waitForAnimationFrame(win);
+
+ let cardThreeToggle = cardThree.querySelector(".extension-enable-button");
+ let addonList = win.document.querySelector("addon-list");
+ // Tab down to cardThreeToggle.
+ while (
+ addonList.contains(win.document.activeElement) &&
+ win.document.activeElement !== cardThreeToggle
+ ) {
+ await pressKey(win, "VK_TAB");
+ }
+ await pressKey(win, " ");
+
+ assertInSection(cardOne, "enabled", "cardOne is still in enabled");
+ assertInSection(cardThree, "enabled", "cardThree is still in enabled");
+
+ transitionsEnded = waitForTransitionEnd(cardOne, cardThree);
+ win.document.querySelector('[action="page-options"]').focus();
+ await transitionsEnded;
+ assertInSection(
+ cardOne,
+ "disabled",
+ "cardOne is now in the disabled section"
+ );
+ assertInSection(
+ cardThree,
+ "disabled",
+ "cardThree is now in the disabled section"
+ );
+
+ // Ensure an uninstalled extension is removed right away.
+ // Hover a card in the middle of the list.
+ await mouseOver(getAddonCard(win, "two@mochi.test"));
+ await cardOne.addon.uninstall(true);
+ ok(!cardOne.parentNode, "cardOne has been removed from the document");
+
+ await closeView(win);
+ await Promise.all(extensions.map(ext => ext.unload()));
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js
new file mode 100644
index 0000000000..b470cf2d82
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+loadTestSubscript("head_abuse_report.js");
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "extensions.abuseReport.amoFormURL",
+ "https://example.org/%LOCALE%/%APP%/feedback/addon/%addonID%/",
+ ],
+ ],
+ });
+
+ // Explicitly flip the amoFormEnabled pref on builds where the pref is
+ // expected to not be set to true by default.
+ if (AppConstants.MOZ_APP_NAME != "firefox") {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", true]],
+ });
+ }
+
+ const { AbuseReporter } = ChromeUtils.importESModule(
+ "resource://gre/modules/AbuseReporter.sys.mjs"
+ );
+
+ Assert.equal(
+ AbuseReporter.amoFormEnabled,
+ true,
+ "Expect AMO abuse report form to be enabled"
+ );
+
+ // Setting up MockProvider to mock various addon types
+ // as installed.
+ await AbuseReportTestUtils.setup();
+});
+
+add_task(async function test_opens_amo_form_in_a_tab() {
+ await openAboutAddons();
+
+ const ADDON_ID = "test-ext@mochitest";
+ const expectedUrl = Services.urlFormatter
+ .formatURLPref("extensions.abuseReport.amoFormURL")
+ .replace("%addonID%", ADDON_ID);
+
+ const promiseWaitForAMOFormTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedUrl
+ );
+ info("Call about:addons openAbuseReport helper function");
+ gManagerWindow.openAbuseReport({ addonId: ADDON_ID });
+ info(`Wait for the AMO form url ${expectedUrl} to be opened in a new tab`);
+ const tab = await promiseWaitForAMOFormTab;
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ expectedUrl,
+ "The newly opened tab has the expected url"
+ );
+ Assert.equal(gBrowser.selectedTab, tab, "The newly opened tab is selected");
+
+ BrowserTestUtils.removeTab(tab);
+ await closeAboutAddons();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_report_button_shown_on_dictionary_addons() {
+ await openAboutAddons("dictionary");
+ await AbuseReportTestUtils.assertReportActionShown(
+ gManagerWindow,
+ EXT_DICTIONARY_ADDON_ID
+ );
+ await closeAboutAddons();
+});
+
+add_task(async function test_report_action_hidden_on_langpack_addons() {
+ await openAboutAddons("locale");
+ await AbuseReportTestUtils.assertReportActionHidden(
+ gManagerWindow,
+ EXT_LANGPACK_ADDON_ID
+ );
+ await closeAboutAddons();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug572561.js b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js
new file mode 100644
index 0000000000..6f8a56bfba
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the locale category is shown if there are no locale packs
+// installed but some are pending install
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+var gInstallProperties = [
+ {
+ name: "Locale Category Test",
+ type: "locale",
+ },
+];
+var gInstall;
+var gExpectedCancel = false;
+var gTestInstallListener = {
+ onInstallStarted(aInstall) {
+ check_hidden(false);
+ },
+
+ onInstallEnded(aInstall) {
+ check_hidden(false);
+ run_next_test();
+ },
+
+ onInstallCancelled(aInstall) {
+ ok(gExpectedCancel, "Should expect install cancel");
+ check_hidden(false);
+ run_next_test();
+ },
+
+ onInstallFailed(aInstall) {
+ ok(false, "Did not expect onInstallFailed");
+ run_next_test();
+ },
+};
+
+async function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ let aWindow = await open_manager("addons://list/extension");
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+}
+
+async function end_test() {
+ await close_manager(gManagerWindow);
+ finish();
+}
+
+function check_hidden(aExpectedHidden) {
+ var hidden = !gCategoryUtilities.isTypeVisible("locale");
+ is(hidden, aExpectedHidden, "Should have correct hidden state");
+}
+
+// Tests that a non-active install does not make the locale category show
+add_test(function () {
+ check_hidden(true);
+ gInstall = gProvider.createInstalls(gInstallProperties)[0];
+ gInstall.addTestListener(gTestInstallListener);
+ check_hidden(true);
+ run_next_test();
+});
+
+// Test that restarting the add-on manager with a non-active install
+// does not cause the locale category to show
+add_test(async function () {
+ let aWindow = await restart_manager(gManagerWindow, null);
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_hidden(true);
+ run_next_test();
+});
+
+// Test that installing the install shows the locale category
+add_test(function () {
+ gInstall.install();
+});
+
+// Test that restarting the add-on manager does not cause the locale category
+// to become hidden
+add_test(async function () {
+ let aWindow = await restart_manager(gManagerWindow, null);
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_hidden(false);
+
+ gExpectedCancel = true;
+ gInstall.cancel();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
new file mode 100644
index 0000000000..9cea5b5045
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that all bundled add-ons are compatible.
+
+async function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ ok(
+ AddonManager.strictCompatibility,
+ "Strict compatibility should be enabled"
+ );
+
+ let aAddons = await AddonManager.getAllAddons();
+ aAddons.sort(function compareTypeName(a, b) {
+ return a.type.localeCompare(b.type) || a.name.localeCompare(b.name);
+ });
+
+ let allCompatible = true;
+ for (let a of aAddons) {
+ // Ignore plugins.
+ if (a.type == "plugin" || a.id == "workerbootstrap-test@mozilla.org") {
+ continue;
+ }
+
+ ok(
+ a.isCompatible,
+ a.type + " " + a.name + " " + a.version + " should be compatible"
+ );
+ allCompatible = allCompatible && a.isCompatible;
+ }
+
+ finish();
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js b/toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js
new file mode 100644
index 0000000000..772e327afc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js
@@ -0,0 +1,265 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../../../browser/base/content/test/webextensions/head.js */
+loadTestSubscript(
+ "../../../../../browser/base/content/test/webextensions/head.js"
+);
+
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const server = AddonTestUtils.createHttpServer();
+
+const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`;
+const EXPIRED_COLORWAY_THEME_ID1 = "2022red-colorway@mozilla.org";
+const EXPIRED_COLORWAY_THEME_ID2 = "2022orange-colorway@mozilla.org";
+const ICON_SVG = `
+ <svg width="63" height="62" viewBox="0 0 63 62" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <circle cx="31.5" cy="31" r="31" fill="url(#paint0_linear)"/>
+ <defs>
+ <linearGradient id="paint0_linear" x1="44.4829" y1="19" x2="10.4829" y2="53" gradientUnits="userSpaceOnUse">
+ <stop stop-color="hsl(147, 94%, 25%)"/>
+ <stop offset="1" stop-color="hsl(146, 38%, 49%)"/>
+ </linearGradient>
+ </defs>
+ </svg>
+`;
+
+AddonTestUtils.registerJSON(server, "/updates.json", {
+ addons: {
+ [EXPIRED_COLORWAY_THEME_ID1]: {
+ updates: [
+ {
+ version: "2.0.0",
+ update_link: `${SERVER_BASE_URL}/${EXPIRED_COLORWAY_THEME_ID1}.xpi`,
+ },
+ ],
+ },
+ [EXPIRED_COLORWAY_THEME_ID2]: {
+ updates: [
+ {
+ version: "3.0.0",
+ update_link: `${SERVER_BASE_URL}/${EXPIRED_COLORWAY_THEME_ID2}.xpi`,
+ },
+ ],
+ },
+ },
+});
+
+const createMockThemeManifest = (id, version) => ({
+ name: `Mock theme ${id} ${version}`,
+ author: "Mozilla",
+ version,
+ icons: { 32: "icon.svg" },
+ theme: {
+ colors: {
+ toolbar: "red",
+ },
+ },
+ browser_specific_settings: {
+ gecko: { id },
+ },
+});
+
+function createWebExtensionFile(id, version) {
+ return AddonTestUtils.createTempWebExtensionFile({
+ files: { "icon.svg": ICON_SVG },
+ manifest: createMockThemeManifest(id, version),
+ });
+}
+
+let expiredThemeUpdate1 = createWebExtensionFile(
+ EXPIRED_COLORWAY_THEME_ID1,
+ "2.0.0"
+);
+let expiredThemeUpdate2 = createWebExtensionFile(
+ EXPIRED_COLORWAY_THEME_ID2,
+ "3.0.0"
+);
+
+server.registerFile(`/${EXPIRED_COLORWAY_THEME_ID1}.xpi`, expiredThemeUpdate1);
+server.registerFile(`/${EXPIRED_COLORWAY_THEME_ID2}.xpi`, expiredThemeUpdate2);
+
+const goBack = async win => {
+ let loaded = waitForViewLoad(win);
+ let backButton = win.document.querySelector(".back-button");
+ ok(!backButton.disabled, "back button is enabled");
+ backButton.click();
+ await loaded;
+};
+
+const assertAddonCardFound = (win, { addonId, expectColorwayBuiltIn }) => {
+ const msg = expectColorwayBuiltIn
+ ? `Found addon card for colorway builtin ${addonId}`
+ : `Found addon card for migrated colorway ${addonId}`;
+
+ Assert.equal(
+ getAddonCard(win, addonId)?.addon.isBuiltinColorwayTheme,
+ expectColorwayBuiltIn,
+ msg
+ );
+};
+
+const assertDetailView = async (win, { addonId, expectThemeName }) => {
+ let loadedDetailView = waitForViewLoad(win);
+ await gBrowser.ownerGlobal.promiseDocumentFlushed(() => {});
+ const themeCard = getAddonCard(win, addonId);
+ // Ensure that we send a click on the control that is accessible (while a
+ // mouse user could also activate a card by clicking on the entire container):
+ const themeCardLink = themeCard.querySelector(".addon-name-link");
+ themeCardLink.click();
+ await loadedDetailView;
+ Assert.equal(
+ themeCard.querySelector(".addon-name")?.textContent,
+ expectThemeName,
+ `Got the expected addon name in the addon details for ${addonId}`
+ );
+};
+
+async function test_update_expired_colorways_builtins() {
+ // Set expired theme as a retained colorway theme
+ const retainedThemePrefName = "browser.theme.retainedExpiredThemes";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_UPDATEURL, `${SERVER_BASE_URL}/updates.json`],
+ ["extensions.checkUpdateSecurity", false],
+ ["browser.theme.colorway-migration", true],
+ [
+ retainedThemePrefName,
+ JSON.stringify([
+ EXPIRED_COLORWAY_THEME_ID1,
+ EXPIRED_COLORWAY_THEME_ID2,
+ ]),
+ ],
+ ],
+ });
+
+ await BuiltInThemes.ensureBuiltInThemes();
+ async function uninstallTestAddons() {
+ for (const addonId of [
+ EXPIRED_COLORWAY_THEME_ID1,
+ EXPIRED_COLORWAY_THEME_ID2,
+ ]) {
+ info(`Uninstalling test theme ${addonId}`);
+ let addon = await AddonManager.getAddonByID(addonId);
+ await addon?.uninstall();
+ }
+ }
+ registerCleanupFunction(uninstallTestAddons);
+
+ const expiredAddon1 = await AddonManager.getAddonByID(
+ EXPIRED_COLORWAY_THEME_ID1
+ );
+ const expiredAddon2 = await AddonManager.getAddonByID(
+ EXPIRED_COLORWAY_THEME_ID2
+ );
+ await expiredAddon2.disable();
+ await expiredAddon1.enable();
+
+ info("Open about:addons theme list view");
+ let win = await loadInitialView("theme");
+
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID1,
+ expectColorwayBuiltIn: true,
+ });
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID2,
+ expectColorwayBuiltIn: true,
+ });
+
+ info("Trigger addon update check");
+ const promiseInstallsEnded = Promise.all([
+ AddonTestUtils.promiseInstallEvent(
+ "onInstallEnded",
+ install => install.addon.id === EXPIRED_COLORWAY_THEME_ID1
+ ),
+ AddonTestUtils.promiseInstallEvent(
+ "onInstallEnded",
+ install => install.addon.id === EXPIRED_COLORWAY_THEME_ID1
+ ),
+ ]);
+ // Wait for active theme to also execute the update bootstrap method.
+ let promiseUpdatedAddon1 = waitForUpdate(expiredAddon1);
+ triggerPageOptionsAction(win, "check-for-updates");
+
+ info("Wait for addon update to be completed");
+ await Promise.all([promiseUpdatedAddon1, promiseInstallsEnded]);
+
+ info("Verify theme list view addon cards");
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID1,
+ expectColorwayBuiltIn: false,
+ });
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID2,
+ expectColorwayBuiltIn: false,
+ });
+
+ info(`Switch to detail view for theme ${EXPIRED_COLORWAY_THEME_ID1}`);
+ await assertDetailView(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID1,
+ expectThemeName: `Mock theme ${EXPIRED_COLORWAY_THEME_ID1} 2.0.0`,
+ });
+
+ info("Switch back to list view");
+ await goBack(win);
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID1,
+ expectColorwayBuiltIn: false,
+ });
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID2,
+ expectColorwayBuiltIn: false,
+ });
+
+ info(`Switch to detail view for theme ${EXPIRED_COLORWAY_THEME_ID2}`);
+ await assertDetailView(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID2,
+ expectThemeName: `Mock theme ${EXPIRED_COLORWAY_THEME_ID2} 3.0.0`,
+ });
+
+ info("Switch back to list view");
+ await goBack(win);
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID1,
+ expectColorwayBuiltIn: false,
+ });
+ assertAddonCardFound(win, {
+ addonId: EXPIRED_COLORWAY_THEME_ID2,
+ expectColorwayBuiltIn: false,
+ });
+
+ Assert.deepEqual(
+ JSON.parse(
+ Services.prefs.getStringPref("browser.theme.retainedExpiredThemes")
+ ),
+ [],
+ "Migrated colorways theme have been removed from the retainedExpiredThemes pref"
+ );
+
+ await closeView(win);
+ await uninstallTestAddons();
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_colorways_builtin_theme_migration() {
+ await test_update_expired_colorways_builtins();
+});
+
+add_task(
+ async function test_colorways_builtin_theme_migration_on_disabledAutoUpdates() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.update.autoUpdateDefault", false]],
+ });
+
+ await test_update_expired_colorways_builtins();
+
+ await SpecialPowers.popPrefEnv();
+ }
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
new file mode 100644
index 0000000000..ae8625a18a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
@@ -0,0 +1,270 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ABOUT_ADDONS_URL = "chrome://mozapps/content/extensions/aboutaddons.html";
+
+const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+);
+
+// Test that the drag-drop-addon-installer component installs add-ons and is
+// included in about:addons. There is an issue with EventUtils.synthesizeDrop
+// where it throws an exception when you give it an subbrowser so we test
+// the component directly.
+
+async function checkInstallConfirmation(...names) {
+ let notificationCount = 0;
+ let observer = {
+ observe(aSubject, aTopic, aData) {
+ let installInfo = aSubject.wrappedJSObject;
+ isnot(
+ installInfo.browser,
+ null,
+ "Notification should have non-null browser"
+ );
+
+ is(
+ installInfo.installs.length,
+ 1,
+ "Got one AddonInstall instance as expected"
+ );
+
+ Assert.deepEqual(
+ installInfo.installs[0].installTelemetryInfo,
+ { source: "about:addons", method: "drag-and-drop" },
+ "Got the expected installTelemetryInfo"
+ );
+
+ notificationCount++;
+ },
+ };
+ Services.obs.addObserver(observer, "addon-install-started");
+
+ let results = [];
+
+ let promise = promisePopupNotificationShown("addon-webext-permissions");
+ for (let i = 0; i < names.length; i++) {
+ let panel = await promise;
+ let name = panel.getAttribute("name");
+ results.push(name);
+
+ info(`Saw install for ${name}`);
+ if (results.length < names.length) {
+ info(
+ `Waiting for installs for ${names.filter(n => !results.includes(n))}`
+ );
+
+ promise = promisePopupNotificationShown("addon-webext-permissions");
+ }
+ panel.secondaryButton.click();
+ }
+
+ Assert.deepEqual(results.sort(), names.sort(), "Got expected installs");
+
+ is(
+ notificationCount,
+ names.length,
+ `Saw ${names.length} addon-install-started notification`
+ );
+ Services.obs.removeObserver(observer, "addon-install-started");
+}
+
+function getDragOverTarget(win) {
+ return win.document.querySelector("categories-box");
+}
+
+function getDropTarget(win) {
+ return win.document.querySelector("drag-drop-addon-installer");
+}
+
+function withTestPage(fn) {
+ return BrowserTestUtils.withNewTab(
+ { url: ABOUT_ADDONS_URL, gBrowser },
+ async browser => {
+ let win = browser.contentWindow;
+ await win.customElements.whenDefined("drag-drop-addon-installer");
+ await fn(browser);
+ }
+ );
+}
+
+function initDragSession({ dragData, dropEffect }) {
+ let dropAction;
+ switch (dropEffect) {
+ case null:
+ case undefined:
+ case "move":
+ dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
+ break;
+ case "copy":
+ dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY;
+ break;
+ case "link":
+ dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK;
+ break;
+ default:
+ throw new Error(`${dropEffect} is an invalid drop effect value`);
+ }
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.dropEffect = dropEffect;
+
+ for (let i = 0; i < dragData.length; i++) {
+ const item = dragData[i];
+ for (let j = 0; j < item.length; j++) {
+ dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+ }
+ }
+
+ dragService.startDragSessionForTests(dropAction);
+ const session = dragService.getCurrentSession();
+ session.dataTransfer = dataTransfer;
+
+ return session;
+}
+
+async function simulateDragAndDrop(win, dragData) {
+ const dropTarget = getDropTarget(win);
+ const dragOverTarget = getDragOverTarget(win);
+ const dropEffect = "move";
+
+ const session = initDragSession({ dragData, dropEffect });
+
+ info("Simulate drag over and wait for the drop target to be visible");
+
+ EventUtils.synthesizeDragOver(
+ dragOverTarget,
+ dragOverTarget,
+ dragData,
+ dropEffect,
+ win
+ );
+
+ // This make sure that the fake dataTransfer has still
+ // the expected drop effect after the synthesizeDragOver call.
+ session.dataTransfer.dropEffect = "move";
+
+ await BrowserTestUtils.waitForCondition(
+ () => !dropTarget.hidden,
+ "Wait for the drop target element to be visible"
+ );
+
+ info("Simulate drop dragData on drop target");
+
+ EventUtils.synthesizeDropAfterDragOver(
+ null,
+ session.dataTransfer,
+ dropTarget,
+ win,
+ { _domDispatchOnly: true }
+ );
+
+ dragService.endDragSession(true);
+}
+
+// Simulates dropping a URL onto the manager
+add_task(async function test_drop_url() {
+ for (let fileType of ["xpi", "zip"]) {
+ await withTestPage(async browser => {
+ const url = TESTROOT + `addons/browser_dragdrop1.${fileType}`;
+ const promise = checkInstallConfirmation("Drag Drop test 1");
+
+ await simulateDragAndDrop(browser.contentWindow, [
+ [{ type: "text/x-moz-url", data: url }],
+ ]);
+
+ await promise;
+ });
+ }
+});
+
+// Simulates dropping a file onto the manager
+add_task(async function test_drop_file() {
+ for (let fileType of ["xpi", "zip"]) {
+ await withTestPage(async browser => {
+ let fileurl = get_addon_file_url(`browser_dragdrop1.${fileType}`);
+ let promise = checkInstallConfirmation("Drag Drop test 1");
+
+ await simulateDragAndDrop(browser.contentWindow, [
+ [{ type: "application/x-moz-file", data: fileurl.file }],
+ ]);
+
+ await promise;
+ });
+ }
+});
+
+// Simulates dropping two urls onto the manager
+add_task(async function test_drop_multiple_urls() {
+ await withTestPage(async browser => {
+ let url1 = TESTROOT + "addons/browser_dragdrop1.xpi";
+ let url2 = TESTROOT2 + "addons/browser_dragdrop2.zip";
+ let promise = checkInstallConfirmation(
+ "Drag Drop test 1",
+ "Drag Drop test 2"
+ );
+
+ await simulateDragAndDrop(browser.contentWindow, [
+ [{ type: "text/x-moz-url", data: url1 }],
+ [{ type: "text/x-moz-url", data: url2 }],
+ ]);
+
+ await promise;
+ });
+}).skip(); // TODO(rpl): this fails because mozSetDataAt throws IndexSizeError.
+
+// Simulates dropping two files onto the manager
+add_task(async function test_drop_multiple_files() {
+ await withTestPage(async browser => {
+ let fileurl1 = get_addon_file_url("browser_dragdrop1.zip");
+ let fileurl2 = get_addon_file_url("browser_dragdrop2.xpi");
+ let promise = checkInstallConfirmation(
+ "Drag Drop test 1",
+ "Drag Drop test 2"
+ );
+
+ await simulateDragAndDrop(browser.contentWindow, [
+ [{ type: "application/x-moz-file", data: fileurl1.file }],
+ [{ type: "application/x-moz-file", data: fileurl2.file }],
+ ]);
+
+ await promise;
+ });
+}).skip(); // TODO(rpl): this fails because mozSetDataAt throws IndexSizeError.
+
+// Simulates dropping a file and a url onto the manager (weird, but should still work)
+add_task(async function test_drop_file_and_url() {
+ await withTestPage(async browser => {
+ let url = TESTROOT + "addons/browser_dragdrop1.xpi";
+ let fileurl = get_addon_file_url("browser_dragdrop2.zip");
+ let promise = checkInstallConfirmation(
+ "Drag Drop test 1",
+ "Drag Drop test 2"
+ );
+
+ await simulateDragAndDrop(browser.contentWindow, [
+ [{ type: "text/x-moz-url", data: url }],
+ [{ type: "application/x-moz-file", data: fileurl.file }],
+ ]);
+
+ await promise;
+ });
+}).skip(); // TODO(rpl): this fails because mozSetDataAt throws IndexSizeError.
+
+// Test that drag-and-drop of an incompatible addon generates
+// an error.
+add_task(async function test_drop_incompat_file() {
+ await withTestPage(async browser => {
+ let url = `${TESTROOT}/addons/browser_dragdrop_incompat.xpi`;
+
+ let panelPromise = promisePopupNotificationShown("addon-install-failed");
+ await simulateDragAndDrop(browser.contentWindow, [
+ [{ type: "text/x-moz-url", data: url }],
+ ]);
+
+ let panel = await panelPromise;
+ ok(panel, "Got addon-install-failed popup");
+ panel.button.click();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js b/toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js
new file mode 100644
index 0000000000..6793363698
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js
@@ -0,0 +1,122 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const ADDON_INSTALL_ID = "addon-webext-permissions";
+
+let fileurl1 = get_addon_file_url("browser_dragdrop1.xpi");
+let fileurl2 = get_addon_file_url("browser_dragdrop2.xpi");
+
+function promiseInstallNotification(aBrowser) {
+ return new Promise(resolve => {
+ function popupshown(event) {
+ let notification = PopupNotifications.getNotification(
+ ADDON_INSTALL_ID,
+ aBrowser
+ );
+ if (!notification) {
+ return;
+ }
+
+ if (gBrowser.selectedBrowser !== aBrowser) {
+ return;
+ }
+
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ ok(true, `Got ${ADDON_INSTALL_ID} popup for browser`);
+ event.target.firstChild.secondaryButton.click();
+ resolve();
+ }
+
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function CheckBrowserInPid(browser, expectedPid, message) {
+ return SpecialPowers.spawn(browser, [{ expectedPid, message }], arg => {
+ is(Services.appinfo.processID, arg.expectedPid, arg.message);
+ });
+}
+
+async function testOpenedAndDraggedXPI(aBrowser) {
+ // Get the current pid for browser for comparison later.
+ let browserPid = await SpecialPowers.spawn(aBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // No process switch for XPI file:// URI in the urlbar.
+ let promiseNotification = promiseInstallNotification(aBrowser);
+ let urlbar = gURLBar;
+ urlbar.value = fileurl1.spec;
+ urlbar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await promiseNotification;
+ await CheckBrowserInPid(
+ aBrowser,
+ browserPid,
+ "Check that browser has not switched process."
+ );
+
+ // No process switch for XPI file:// URI dragged to tab.
+ let tab = gBrowser.getTabForBrowser(aBrowser);
+ promiseNotification = promiseInstallNotification(aBrowser);
+ let effect = EventUtils.synthesizeDrop(
+ tab,
+ tab,
+ [[{ type: "text/uri-list", data: fileurl1.spec }]],
+ "move"
+ );
+ is(effect, "move", "Drag should be accepted");
+ await promiseNotification;
+ await CheckBrowserInPid(
+ aBrowser,
+ browserPid,
+ "Check that browser has not switched process."
+ );
+
+ // No process switch for two XPI file:// URIs dragged to tab.
+ promiseNotification = promiseInstallNotification(aBrowser);
+ let promiseNewTab = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ effect = EventUtils.synthesizeDrop(
+ tab,
+ tab,
+ [
+ [{ type: "text/uri-list", data: fileurl1.spec }],
+ [{ type: "text/uri-list", data: fileurl2.spec }],
+ ],
+ "move"
+ );
+ is(effect, "move", "Drag should be accepted");
+ // When drag'n'dropping two XPIs, one is loaded in the current tab while the
+ // other one is loaded in a new tab.
+ let { target: newTab } = await promiseNewTab;
+ // This is the prompt for the first XPI in the current tab.
+ await promiseNotification;
+
+ let promiseSecondNotification = promiseInstallNotification(
+ newTab.linkedBrowser
+ );
+
+ // We switch to the second tab and wait for the prompt for the second XPI.
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ await promiseSecondNotification;
+
+ BrowserTestUtils.removeTab(newTab);
+
+ await CheckBrowserInPid(
+ aBrowser,
+ browserPid,
+ "Check that browser has not switched process."
+ );
+}
+
+// Test for bug 1175267.
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "http://example.com",
+ testOpenedAndDraggedXPI
+ );
+ await BrowserTestUtils.withNewTab("about:robots", testOpenedAndDraggedXPI);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js b/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js
new file mode 100644
index 0000000000..368160698f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 566194 - safe mode / security & compatibility check status are not exposed in new addon manager UI
+
+async function loadDetail(win, id) {
+ let loaded = waitForViewLoad(win);
+ // Check the detail view.
+ let card = win.document.querySelector(`addon-card[addon-id="${id}"]`);
+ EventUtils.synthesizeMouseAtCenter(
+ card.querySelector(".addon-name-link"),
+ {},
+ win
+ );
+ await loaded;
+}
+
+function checkMessageShown(win, type, hasButton) {
+ let stack = win.document.querySelector("global-warnings");
+ is(stack.childElementCount, 1, "There is one message");
+ let messageBar = stack.firstElementChild;
+ ok(messageBar, "There is a message bar");
+ is(
+ messageBar.localName,
+ "moz-message-bar",
+ "The message bar is a moz-message-bar"
+ );
+ is_element_visible(messageBar, "Message bar is visible");
+ is(messageBar.getAttribute("warning-type"), type);
+ if (hasButton) {
+ let button = messageBar.querySelector("button");
+ is_element_visible(button, "Button is visible");
+ is(button.getAttribute("action"), type, "Button action is set");
+ }
+}
+
+function checkNoMessages(win) {
+ let stack = win.document.querySelector("global-warnings");
+ if (stack.childElementCount) {
+ // The safe mode message is hidden in CSS on the plugin list.
+ for (let child of stack.children) {
+ is_element_hidden(child, "The message is hidden");
+ }
+ } else {
+ is(stack.childElementCount, 0, "There are no message bars");
+ }
+}
+
+function clickMessageAction(win) {
+ let stack = win.document.querySelector("global-warnings");
+ let button = stack.firstElementChild.querySelector("button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+}
+
+add_task(async function checkCompatibility() {
+ info("Testing compatibility checking warning");
+
+ info("Setting checkCompatibility to false");
+ AddonManager.checkCompatibility = false;
+
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+
+ // Check the extension list view.
+ checkMessageShown(win, "check-compatibility", true);
+
+ // Check the detail view.
+ await loadDetail(win, id);
+ checkMessageShown(win, "check-compatibility", true);
+
+ // Check other views.
+ let views = ["plugin", "theme"];
+ for (let view of views) {
+ await switchView(win, view);
+ checkMessageShown(win, "check-compatibility", true);
+ }
+
+ // Check the button works.
+ info("Clicking 'Enable' button");
+ clickMessageAction(win);
+ is(
+ AddonManager.checkCompatibility,
+ true,
+ "Check Compatibility pref should be cleared"
+ );
+ checkNoMessages(win);
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function checkSecurity() {
+ info("Testing update security checking warning");
+
+ var pref = "extensions.checkUpdateSecurity";
+ info("Setting " + pref + " pref to false");
+ await SpecialPowers.pushPrefEnv({
+ set: [[pref, false]],
+ });
+
+ let id = "test-security@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+
+ // Check extension list view.
+ checkMessageShown(win, "update-security", true);
+
+ // Check detail view.
+ await loadDetail(win, id);
+ checkMessageShown(win, "update-security", true);
+
+ // Check other views.
+ let views = ["plugin", "theme"];
+ for (let view of views) {
+ await switchView(win, view);
+ checkMessageShown(win, "update-security", true);
+ }
+
+ // Check the button works.
+ info("Clicking 'Enable' button");
+ clickMessageAction(win);
+ is(
+ Services.prefs.prefHasUserValue(pref),
+ false,
+ "Check Update Security pref should be cleared"
+ );
+ checkNoMessages(win);
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function checkSafeMode() {
+ info("Testing safe mode warning");
+
+ let id = "test-safemode@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+
+ // Check extension list view hidden.
+ checkNoMessages(win);
+
+ let globalWarnings = win.document.querySelector("global-warnings");
+ globalWarnings.inSafeMode = true;
+ globalWarnings.refresh();
+
+ // Check detail view.
+ await loadDetail(win, id);
+ checkMessageShown(win, "safe-mode");
+
+ // Check other views.
+ await switchView(win, "theme");
+ checkMessageShown(win, "safe-mode");
+ await switchView(win, "plugin");
+ checkNoMessages(win);
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js
new file mode 100644
index 0000000000..51ffbc6cdd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js
@@ -0,0 +1,406 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { GMPInstallManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/GMPInstallManager.sys.mjs"
+);
+const { GMPPrefs, GMP_PLUGIN_IDS, WIDEVINE_L1_ID, WIDEVINE_L3_ID } =
+ ChromeUtils.importESModule("resource://gre/modules/GMPUtils.sys.mjs");
+
+const TEST_DATE = new Date(2013, 0, 1, 12);
+
+var gMockAddons = [];
+
+for (let pluginId of GMP_PLUGIN_IDS) {
+ let mockAddon = Object.freeze({
+ id: pluginId,
+ isValid: true,
+ isInstalled: false,
+ isEME: pluginId == WIDEVINE_L1_ID || pluginId == WIDEVINE_L3_ID,
+ usedFallback: true,
+ });
+ gMockAddons.push(mockAddon);
+}
+
+var gInstalledAddonId = "";
+var gInstallDeferred = null;
+var gPrefs = Services.prefs;
+var getKey = GMPPrefs.getPrefKey;
+
+const MockGMPInstallManagerPrototype = {
+ checkForAddons: () =>
+ Promise.resolve({
+ addons: gMockAddons,
+ }),
+
+ installAddon: addon => {
+ gInstalledAddonId = addon.id;
+ gInstallDeferred.resolve();
+ return Promise.resolve();
+ },
+};
+
+function openDetailsView(win, id) {
+ let item = getAddonCard(win, id);
+ Assert.ok(item, "Should have got add-on element.");
+ is_element_visible(item, "Add-on element should be visible.");
+
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(
+ item.querySelector(".addon-name-link"),
+ {},
+ item.ownerGlobal
+ );
+ return loaded;
+}
+
+add_task(async function initializeState() {
+ gPrefs.setBoolPref(GMPPrefs.KEY_LOGGING_DUMP, true);
+ gPrefs.setIntPref(GMPPrefs.KEY_LOGGING_LEVEL, 0);
+
+ registerCleanupFunction(async function () {
+ for (let addon of gMockAddons) {
+ gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id));
+ gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id));
+ gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id));
+ gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id));
+ gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id));
+ gPrefs.clearUserPref(
+ getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id)
+ );
+ }
+ gPrefs.clearUserPref(GMPPrefs.KEY_LOGGING_DUMP);
+ gPrefs.clearUserPref(GMPPrefs.KEY_LOGGING_LEVEL);
+ gPrefs.clearUserPref(GMPPrefs.KEY_UPDATE_LAST_CHECK);
+ gPrefs.clearUserPref(GMPPrefs.KEY_EME_ENABLED);
+ });
+
+ // Start out with plugins not being installed, disabled and automatic updates
+ // disabled.
+ gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, true);
+ for (let addon of gMockAddons) {
+ gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false);
+ gPrefs.setIntPref(getKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id), 0);
+ gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false);
+ gPrefs.setCharPref(getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), "");
+ gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true);
+ gPrefs.setBoolPref(
+ getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id),
+ true
+ );
+ }
+});
+
+add_task(async function testNotInstalledDisabled() {
+ let win = await loadInitialView("extension");
+
+ Assert.ok(isCategoryVisible(win, "plugin"), "Plugin tab visible.");
+ await switchView(win, "plugin");
+
+ for (let addon of gMockAddons) {
+ let addonCard = getAddonCard(win, addon.id);
+ Assert.ok(addonCard, "Got add-on element:" + addon.id);
+
+ is(
+ addonCard.ownerDocument.l10n.getAttributes(addonCard.addonNameEl).id,
+ "addon-name-disabled",
+ "The addon name should include a disabled postfix"
+ );
+
+ let cardMessage = addonCard.querySelector(
+ "moz-message-bar.addon-card-message"
+ );
+ is_element_hidden(cardMessage, "Warning notification is hidden");
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testNotInstalledDisabledDetails() {
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ await openDetailsView(win, addon.id);
+ let addonCard = getAddonCard(win, addon.id);
+ ok(addonCard, "Got add-on element: " + addon.id);
+
+ is(
+ win.document.l10n.getAttributes(addonCard.addonNameEl).id,
+ "addon-name-disabled",
+ "The addon name should include a disabled postfix"
+ );
+
+ let updatesBtn = addonCard.querySelector("[action=update-check]");
+ is_element_visible(updatesBtn, "Check for Updates action is visible");
+ let cardMessage = addonCard.querySelector(
+ "moz-message-bar.addon-card-message"
+ );
+ is_element_hidden(cardMessage, "Warning notification is hidden");
+
+ await switchView(win, "plugin");
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testNotInstalled() {
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true);
+ let item = getAddonCard(win, addon.id);
+ Assert.ok(item, "Got add-on element:" + addon.id);
+
+ let warningMessageBar = await BrowserTestUtils.waitForCondition(() => {
+ return item.querySelector(
+ "moz-message-bar.addon-card-message[type=warning]"
+ );
+ }, "Wait for the addon card message to be updated");
+
+ is_element_visible(warningMessageBar, "Warning notification is visible");
+
+ is(item.parentNode.getAttribute("section"), "0", "Should be enabled");
+ // Open the options menu (needed to check the disabled buttons).
+ const pluginOptions = item.querySelector("plugin-options");
+ pluginOptions.querySelector("panel-list").open = true;
+ const alwaysActivate = pluginOptions.querySelector(
+ "panel-item[action=always-activate]"
+ );
+ ok(
+ alwaysActivate.hasAttribute("checked"),
+ "Plugin state should be always-activate"
+ );
+ pluginOptions.querySelector("panel-list").open = false;
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testNotInstalledDetails() {
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ await openDetailsView(win, addon.id);
+
+ const addonCard = getAddonCard(win, addon.id);
+ let el = addonCard.querySelector("[action=update-check]");
+ is_element_visible(el, "Check for Updates action is visible");
+
+ let warningMessageBar = await BrowserTestUtils.waitForCondition(() => {
+ return addonCard.querySelector(
+ "moz-message-bar.addon-card-message[type=warning]"
+ );
+ }, "Wait for the addon card message to be updated");
+ is_element_visible(warningMessageBar, "Warning notification is visible");
+
+ await switchView(win, "plugin");
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testInstalled() {
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ gPrefs.setIntPref(
+ getKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id),
+ TEST_DATE.getTime()
+ );
+ gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false);
+ gPrefs.setCharPref(
+ getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ "1.2.3.4"
+ );
+
+ let item = getAddonCard(win, addon.id);
+ Assert.ok(item, "Got add-on element.");
+
+ is(item.parentNode.getAttribute("section"), "0", "Should be enabled");
+ // Open the options menu (needed to check the disabled buttons).
+ const pluginOptions = item.querySelector("plugin-options");
+ pluginOptions.querySelector("panel-list").open = true;
+ const alwaysActivate = pluginOptions.querySelector(
+ "panel-item[action=always-activate]"
+ );
+ ok(
+ alwaysActivate.hasAttribute("checked"),
+ "Plugin state should be always-activate"
+ );
+ pluginOptions.querySelector("panel-list").open = false;
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testInstalledDetails() {
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ await openDetailsView(win, addon.id);
+
+ let card = getAddonCard(win, addon.id);
+ ok(card, "Got add-on element:" + addon.id);
+
+ is_element_visible(
+ card.querySelector("[action=update-check]"),
+ "Find updates link is visible"
+ );
+
+ await switchView(win, "plugin");
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testInstalledGlobalEmeDisabled() {
+ let win = await loadInitialView("plugin");
+ gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, false);
+
+ for (let addon of gMockAddons) {
+ let item = getAddonCard(win, addon.id);
+ if (addon.isEME) {
+ is(item.parentNode.getAttribute("section"), "1", "Should be disabled");
+ } else {
+ Assert.ok(item, "Got add-on element.");
+ }
+ }
+
+ gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, true);
+ await closeView(win);
+});
+
+add_task(async function testPreferencesButton() {
+ let prefValues = [
+ { enabled: false, version: "" },
+ { enabled: false, version: "1.2.3.4" },
+ { enabled: true, version: "" },
+ { enabled: true, version: "1.2.3.4" },
+ ];
+
+ for (let preferences of prefValues) {
+ info(
+ "Testing preferences button with pref settings: " +
+ JSON.stringify(preferences)
+ );
+ for (let addon of gMockAddons) {
+ let win = await loadInitialView("plugin");
+ gPrefs.setCharPref(
+ getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ preferences.version
+ );
+ gPrefs.setBoolPref(
+ getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id),
+ preferences.enabled
+ );
+
+ let item = getAddonCard(win, addon.id);
+
+ // Open the options menu (needed to check the more options action is enabled).
+ const pluginOptions = item.querySelector("plugin-options");
+ pluginOptions.querySelector("panel-list").open = true;
+ const moreOptions = pluginOptions.querySelector(
+ "panel-item[action=expand]"
+ );
+ ok(
+ !moreOptions.shadowRoot.querySelector("button").disabled,
+ "more options action should be enabled"
+ );
+ moreOptions.click();
+
+ await waitForViewLoad(win);
+
+ item = getAddonCard(win, addon.id);
+ ok(item, "The right view is loaded");
+
+ await closeView(win);
+ }
+ }
+});
+
+add_task(async function testUpdateButton() {
+ gPrefs.clearUserPref(GMPPrefs.KEY_UPDATE_LAST_CHECK);
+
+ // The GMPInstallManager constructor has an empty body,
+ // so replacing the prototype is safe.
+ let originalInstallManager = GMPInstallManager.prototype;
+ GMPInstallManager.prototype = MockGMPInstallManagerPrototype;
+
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ let item = getAddonCard(win, addon.id);
+
+ gInstalledAddonId = "";
+ gInstallDeferred = Promise.withResolvers();
+
+ let loaded = waitForViewLoad(win);
+ item.querySelector("[action=expand]").click();
+ await loaded;
+ let detail = getAddonCard(win, addon.id);
+ detail.querySelector("[action=update-check]").click();
+
+ await gInstallDeferred.promise;
+ Assert.equal(gInstalledAddonId, addon.id);
+
+ await switchView(win, "plugin");
+ }
+
+ GMPInstallManager.prototype = originalInstallManager;
+
+ await closeView(win);
+});
+
+add_task(async function testEmeSupport() {
+ for (let addon of gMockAddons) {
+ gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id));
+ }
+
+ let win = await loadInitialView("plugin");
+
+ for (let addon of gMockAddons) {
+ let item = getAddonCard(win, addon.id);
+ if (addon.id == WIDEVINE_L1_ID) {
+ if (
+ AppConstants.MOZ_WMF_CDM &&
+ AppConstants.platform == "win" &&
+ UpdateUtils.ABI.match(/x64/)
+ ) {
+ Assert.ok(item, "Widevine L1 supported, found add-on element.");
+ } else {
+ Assert.ok(
+ !item,
+ "Widevine L1 not supported, couldn't find add-on element."
+ );
+ }
+ } else if (addon.id == WIDEVINE_L3_ID) {
+ if (
+ AppConstants.platform == "win" ||
+ AppConstants.platform == "macosx" ||
+ AppConstants.platform == "linux"
+ ) {
+ Assert.ok(item, "Widevine L3 supported, found add-on element.");
+ } else {
+ Assert.ok(
+ !item,
+ "Widevine L3 not supported, couldn't find add-on element."
+ );
+ }
+ } else {
+ Assert.ok(item, "Found add-on element.");
+ }
+ }
+
+ await closeView(win);
+
+ for (let addon of gMockAddons) {
+ gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true);
+ gPrefs.setBoolPref(
+ getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id),
+ true
+ );
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_history_navigation.js b/toolkit/mozapps/extensions/test/browser/browser_history_navigation.js
new file mode 100644
index 0000000000..2b177bc7cd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_history_navigation.js
@@ -0,0 +1,623 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* eslint max-nested-callbacks: ["warn", 12] */
+
+/**
+ * Tests that history navigation works for the add-ons manager.
+ */
+
+// Request a longer timeout, because this tests run twice
+// (once on XUL views and once on the HTML views).
+requestLongerTimeout(4);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const DISCOAPI_URL = `http://example.com/${RELATIVE_DIR}/discovery/api_response_empty.json`;
+
+SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+});
+
+var gProvider = new MockProvider();
+gProvider.createAddons([
+ {
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on 1",
+ description: "foo",
+ },
+ {
+ id: "test2@tests.mozilla.org",
+ name: "Test add-on 2",
+ description: "bar",
+ },
+ {
+ id: "test3@tests.mozilla.org",
+ name: "Test add-on 3",
+ type: "theme",
+ description: "bar",
+ },
+]);
+
+function go_back() {
+ gBrowser.goBack();
+}
+
+const goBackKeyModifier =
+ AppConstants.platform == "macosx" ? { metaKey: true } : { altKey: true };
+
+function go_back_key() {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", goBackKeyModifier);
+}
+
+function go_forward_key() {
+ EventUtils.synthesizeKey("KEY_ArrowRight", goBackKeyModifier);
+}
+
+function go_forward() {
+ gBrowser.goForward();
+}
+
+function check_state(canGoBack, canGoForward) {
+ is(gBrowser.canGoBack, canGoBack, "canGoBack should be correct");
+ is(gBrowser.canGoForward, canGoForward, "canGoForward should be correct");
+}
+
+function is_in_list(aManager, view, canGoBack, canGoForward) {
+ var categoryUtils = new CategoryUtilities(aManager);
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ view,
+ "Should be on the right category"
+ );
+
+ ok(
+ aManager.document.querySelector("addon-list"),
+ "Got a list-view in about:addons"
+ );
+
+ check_state(canGoBack, canGoForward);
+}
+
+function is_in_detail(aManager, view, canGoBack, canGoForward) {
+ var categoryUtils = new CategoryUtilities(aManager);
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ view,
+ "Should be on the right category"
+ );
+
+ is(
+ aManager.document.querySelectorAll("addon-card").length,
+ 1,
+ "Got a detail-view in about:addons"
+ );
+
+ check_state(canGoBack, canGoForward);
+}
+
+function is_in_discovery(aManager, canGoBack, canGoForward) {
+ ok(
+ aManager.document.querySelector("discovery-pane"),
+ "Got a discovery panel in the HTML about:addons browser"
+ );
+
+ check_state(canGoBack, canGoForward);
+}
+
+async function expand_addon_element(aManagerWin, aId) {
+ var addon = getAddonCard(aManagerWin, aId);
+ // Ensure that we send a click on the control that is accessible (while a
+ // mouse user could also activate a card by clicking on the entire container):
+ const addonLink = addon.querySelector(".addon-name-link");
+ addonLink.click();
+}
+
+function wait_for_page_load(browser) {
+ return BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
+}
+
+// Tests simple forward and back navigation and that the right heading and
+// category is selected
+add_task(async function test_navigate_history() {
+ let aManager = await open_manager("addons://list/extension");
+ let categoryUtils = new CategoryUtilities(aManager);
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager);
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 3");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ go_forward();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 4");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 5");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ await expand_addon_element(aManager, "test1@tests.mozilla.org");
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 6");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 7");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ await close_manager(aManager);
+});
+
+// Tests that browsing to the add-ons manager from a website and going back works
+add_task(async function test_navigate_between_webpage_and_aboutaddons() {
+ info("Part 1");
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/",
+ true,
+ true
+ );
+
+ info("Part 2");
+ ok(!gBrowser.canGoBack, "Should not be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "about:addons"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let manager = await wait_for_manager_load(
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ info("Part 3");
+ is_in_list(manager, "addons://list/extension", true, false);
+
+ // XXX: This is less than ideal, as it's currently difficult to deal with
+ // the browser frame switching between remote/non-remote in e10s mode.
+ let promiseLoaded;
+ if (gMultiProcessBrowser) {
+ promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ } else {
+ promiseLoaded = BrowserTestUtils.waitForEvent(
+ gBrowser.selectedBrowser,
+ "pageshow"
+ );
+ }
+
+ go_back(manager);
+ await promiseLoaded;
+
+ info("Part 4");
+ is(
+ gBrowser.currentURI.spec,
+ "http://example.com/",
+ "Should be showing the webpage"
+ );
+ ok(!gBrowser.canGoBack, "Should not be able to go back");
+ ok(gBrowser.canGoForward, "Should be able to go forward");
+
+ promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ go_forward(manager);
+ await promiseLoaded;
+
+ manager = gBrowser.selectedBrowser.contentWindow;
+ info("Part 5");
+ await TestUtils.waitForCondition(
+ () => manager.document.querySelector("addon-list"),
+ "The add-on list should render."
+ );
+
+ is_in_list(manager, "addons://list/extension", true, false);
+
+ await close_manager(manager);
+});
+
+// Tests simple forward and back navigation and that the right heading and
+// category is selected -- Keyboard navigation [Bug 565359]
+add_task(async function test_keyboard_history_navigation() {
+ let aManager = await open_manager("addons://list/extension");
+ let categoryUtils = new CategoryUtilities(aManager);
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager);
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ // Backspace should not navigate back. We should still be on the same view.
+ is(
+ Services.prefs.getIntPref("browser.backspace_action"),
+ 2,
+ "Backspace should not navigate back"
+ );
+ EventUtils.synthesizeKey("KEY_Backspace");
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2b");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back_key();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 3");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ go_forward_key();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 4");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back_key();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 5");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ await expand_addon_element(aManager, "test1@tests.mozilla.org");
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 6");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ go_back_key();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 7");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ await close_manager(aManager);
+});
+
+// Tests that opening a custom first view only stores a single history entry
+add_task(async function test_single_history_entry() {
+ let aManager = await open_manager("addons://list/plugin");
+ let categoryUtils = new CategoryUtilities(aManager);
+ info("Part 1");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(
+ categoryUtils.get("extension"),
+ {},
+ aManager
+ );
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_list(aManager, "addons://list/extension", true, false);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 3");
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ await close_manager(aManager);
+});
+
+// Tests that opening a view while the manager is already open adds a new
+// history entry
+add_task(async function test_new_history_entry_while_opened() {
+ let aManager = await open_manager("addons://list/extension");
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ aManager.loadView("addons://list/plugin");
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 3");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ go_forward();
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 4");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ await close_manager(aManager);
+});
+
+// Tests than navigating to a website and then going back returns to the
+// previous view
+add_task(async function test_navigate_back_from_website() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+ });
+
+ let aManager = await open_manager("addons://list/plugin");
+ info("Part 1");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ BrowserTestUtils.startLoadingURIString(gBrowser, "http://example.com/");
+ await wait_for_page_load(gBrowser.selectedBrowser);
+
+ info("Part 2");
+
+ await new Promise(resolve =>
+ executeSoon(function () {
+ ok(gBrowser.canGoBack, "Should be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ go_back();
+
+ gBrowser.addEventListener("pageshow", async function listener(event) {
+ if (event.target.location != "about:addons") {
+ return;
+ }
+ gBrowser.removeEventListener("pageshow", listener);
+
+ aManager = await wait_for_view_load(
+ gBrowser.contentWindow.wrappedJSObject
+ );
+ info("Part 3");
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ executeSoon(() => go_forward());
+ wait_for_page_load(gBrowser.selectedBrowser).then(() => {
+ info("Part 4");
+
+ executeSoon(function () {
+ ok(gBrowser.canGoBack, "Should be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ go_back();
+
+ gBrowser.addEventListener(
+ "pageshow",
+ async function listener(event) {
+ if (event.target.location != "about:addons") {
+ return;
+ }
+ gBrowser.removeEventListener("pageshow", listener);
+ aManager = await wait_for_view_load(
+ gBrowser.contentWindow.wrappedJSObject
+ );
+ info("Part 5");
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ resolve();
+ }
+ );
+ });
+ });
+ });
+ })
+ );
+
+ await close_manager(aManager);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that refreshing a list view does not affect the history
+add_task(async function test_refresh_listview_donot_add_history_entries() {
+ let aManager = await open_manager("addons://list/extension");
+ let categoryUtils = new CategoryUtilities(aManager);
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager);
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ await new Promise(resolve => {
+ gBrowser.reload();
+ gBrowser.addEventListener("pageshow", async function listener(event) {
+ if (event.target.location != "about:addons") {
+ return;
+ }
+ gBrowser.removeEventListener("pageshow", listener);
+
+ aManager = await wait_for_view_load(
+ gBrowser.contentWindow.wrappedJSObject
+ );
+ info("Part 3");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back();
+ aManager = await wait_for_view_load(aManager);
+ info("Part 4");
+ is_in_list(aManager, "addons://list/extension", false, true);
+ resolve();
+ });
+ });
+
+ await close_manager(aManager);
+});
+
+// Tests that refreshing a detail view does not affect the history
+add_task(async function test_refresh_detailview_donot_add_history_entries() {
+ let aManager = await open_manager(null);
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ await expand_addon_element(aManager, "test1@tests.mozilla.org");
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ await new Promise(resolve => {
+ gBrowser.reload();
+ gBrowser.addEventListener("pageshow", async function listener(event) {
+ if (event.target.location != "about:addons") {
+ return;
+ }
+ gBrowser.removeEventListener("pageshow", listener);
+
+ aManager = await wait_for_view_load(
+ gBrowser.contentWindow.wrappedJSObject
+ );
+ info("Part 3");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ go_back();
+ aManager = await wait_for_view_load(aManager);
+ info("Part 4");
+ is_in_list(aManager, "addons://list/extension", false, true);
+ resolve();
+ });
+ });
+
+ await close_manager(aManager);
+});
+
+// Tests that removing an extension from the detail view goes back and doesn't
+// allow you to go forward again.
+add_task(async function test_history_on_detailview_extension_removed() {
+ let aManager = await open_manager("addons://list/extension");
+
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ await expand_addon_element(aManager, "test1@tests.mozilla.org");
+
+ aManager = await wait_for_view_load(aManager);
+ info("Part 2");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ const addonCard = aManager.document.querySelector(
+ 'addon-card[addon-id="test1@tests.mozilla.org"]'
+ );
+ const promptService = mockPromptService();
+ promptService._response = 0;
+ addonCard.querySelector("[action=remove]").click();
+
+ await wait_for_view_load(aManager);
+ await TestUtils.waitForCondition(
+ () => aManager.document.querySelector("addon-list"),
+ "The add-on list should render."
+ );
+ is_in_list(aManager, "addons://list/extension", true, false);
+
+ const addon = await AddonManager.getAddonByID("test1@tests.mozilla.org");
+ addon.cancelUninstall();
+
+ await close_manager(aManager);
+});
+
+// Tests that opening the manager opens the last view
+add_task(async function test_open_last_view() {
+ let aManager = await open_manager("addons://list/plugin");
+ info("Part 1");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ await close_manager(aManager);
+ aManager = await open_manager(null);
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ await close_manager(aManager);
+});
+
+// Tests that navigating the discovery page works when that was the first view
+add_task(async function test_discopane_first_history_entry() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.getAddons.discovery.api_url", DISCOAPI_URL]],
+ });
+
+ let aManager = await open_manager("addons://discover/");
+ let categoryUtils = new CategoryUtilities(aManager);
+ info("1");
+ is_in_discovery(aManager, false, false);
+
+ EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager);
+
+ aManager = await wait_for_view_load(aManager);
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back();
+ aManager = await wait_for_view_load(aManager);
+
+ is_in_discovery(aManager, false, true);
+
+ await close_manager(aManager);
+});
+
+// Tests that navigating the discovery page works when that was the second view
+add_task(async function test_discopane_second_history_entry() {
+ let aManager = await open_manager("addons://list/plugin");
+ let categoryUtils = new CategoryUtilities(aManager);
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(
+ categoryUtils.get("discover"),
+ {},
+ aManager
+ );
+
+ aManager = await wait_for_view_load(aManager);
+ is_in_discovery(aManager, true, false);
+
+ EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager);
+
+ aManager = await wait_for_view_load(aManager);
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ is_in_discovery(aManager, true, true);
+
+ go_back();
+
+ aManager = await wait_for_view_load(aManager);
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ await close_manager(aManager);
+});
+
+add_task(async function test_initialSelectedView_on_aboutaddons_reload() {
+ let managerWindow = await open_manager("addons://list/extension");
+ isnot(
+ managerWindow.gViewController.currentViewId,
+ null,
+ "Got a non null currentViewId on first load"
+ );
+
+ managerWindow.location.reload();
+ await wait_for_manager_load(managerWindow);
+ await wait_for_view_load(managerWindow);
+
+ isnot(
+ managerWindow.gViewController.currentViewId,
+ null,
+ "Got a non null currentViewId on reload"
+ );
+
+ await close_manager(managerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
new file mode 100644
index 0000000000..3ad8510aea
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
@@ -0,0 +1,1093 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+loadTestSubscript("head_abuse_report.js");
+
+add_setup(async function () {
+ // Make sure the integrated abuse report panel is the one enabled
+ // while this test file runs (instead of the AMO hosted form).
+ // NOTE: behaviors expected when amoFormEnabled is true are tested
+ // in the separate browser_amo_abuse_report.js test file.
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", false]],
+ });
+ await AbuseReportTestUtils.setup();
+});
+
+/**
+ * Base tests on abuse report panel webcomponents.
+ */
+
+// This test case verified that the abuse report panels contains a radio
+// button for all the expected "abuse report reasons", they are grouped
+// together under the same form field named "reason".
+add_task(async function test_abusereport_issuelist() {
+ const extension = await installTestExtension();
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id);
+
+ const reasonsPanel = abuseReportEl._reasonsPanel;
+ const radioButtons = reasonsPanel.querySelectorAll("[type=radio]");
+ const selectedRadios = reasonsPanel.querySelectorAll("[type=radio]:checked");
+
+ is(selectedRadios.length, 1, "Expect only one radio button selected");
+ is(
+ selectedRadios[0],
+ radioButtons[0],
+ "Expect the first radio button to be selected"
+ );
+
+ is(
+ abuseReportEl.reason,
+ radioButtons[0].value,
+ `The reason property has the expected value: ${radioButtons[0].value}`
+ );
+
+ const reasons = Array.from(radioButtons).map(el => el.value);
+ Assert.deepEqual(
+ reasons.sort(),
+ AbuseReportTestUtils.getReasons(abuseReportEl).sort(),
+ `Got a radio button for the expected reasons`
+ );
+
+ for (const radio of radioButtons) {
+ const reasonInfo = AbuseReportTestUtils.getReasonInfo(
+ abuseReportEl,
+ radio.value
+ );
+ const expectExampleHidden =
+ reasonInfo && reasonInfo.isExampleHidden("extension");
+ is(
+ radio.parentNode.querySelector(".reason-example").hidden,
+ expectExampleHidden,
+ `Got expected visibility on the example for reason "${radio.value}"`
+ );
+ }
+
+ info("Change the selected reason to " + radioButtons[3].value);
+ radioButtons[3].checked = true;
+ is(
+ abuseReportEl.reason,
+ radioButtons[3].value,
+ "The reason property has the expected value"
+ );
+
+ await extension.unload();
+ await closeAboutAddons();
+});
+
+// This test case verifies that the abuse report panel:
+// - switches from its "reasons list" mode to its "submit report" mode when the
+// "next" button is clicked
+// - goes back to the "reasons list" mode when the "go back" button is clicked
+// - the abuse report panel is closed when the "close" icon is clicked
+add_task(async function test_abusereport_submitpanel() {
+ const extension = await installTestExtension();
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id);
+
+ ok(
+ !abuseReportEl._reasonsPanel.hidden,
+ "The list of abuse reasons is the currently visible"
+ );
+ ok(
+ abuseReportEl._submitPanel.hidden,
+ "The submit panel is the currently hidden"
+ );
+
+ let onceUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ abuseReportEl,
+ "submit"
+ );
+ const MozButtonGroup =
+ abuseReportEl.ownerGlobal.customElements.get("moz-button-group");
+
+ ok(MozButtonGroup, "Expect MozButtonGroup custom element to be defined");
+
+ const assertButtonInMozButtonGroup = (
+ btnEl,
+ { expectPrimary = false } = {}
+ ) => {
+ // Let's include the l10n id into the assertion messages,
+ // to make it more likely to be immediately clear which
+ // button hit a failure if any of the following assertion
+ // fails.
+ let l10nId = btnEl.getAttribute("data-l10n-id");
+ is(
+ btnEl.classList.contains("primary"),
+ expectPrimary,
+ `Expect button ${l10nId} to have${
+ expectPrimary ? "" : " NOT"
+ } the primary class set`
+ );
+
+ ok(
+ btnEl.parentElement instanceof MozButtonGroup,
+ `Expect button ${l10nId} to be slotted inside the expected custom element`
+ );
+
+ is(
+ btnEl.getAttribute("slot"),
+ expectPrimary ? "primary" : null,
+ `Expect button ${l10nId} slot to ${
+ expectPrimary ? "" : "NOT "
+ } be set to primary`
+ );
+ };
+
+ // Verify button group from the initial panel.
+ assertButtonInMozButtonGroup(abuseReportEl._btnNext, { expectPrimary: true });
+ assertButtonInMozButtonGroup(abuseReportEl._btnCancel, {
+ expectPrimary: false,
+ });
+ await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext);
+ await onceUpdated;
+ // Verify button group from the submit panel mode.
+ assertButtonInMozButtonGroup(abuseReportEl._btnSubmit, {
+ expectPrimary: true,
+ });
+ assertButtonInMozButtonGroup(abuseReportEl._btnGoBack, {
+ expectPrimary: false,
+ });
+ onceUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ abuseReportEl,
+ "reasons"
+ );
+ await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnGoBack);
+ await onceUpdated;
+
+ const onceReportClosed =
+ AbuseReportTestUtils.promiseReportClosed(abuseReportEl);
+ await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnCancel);
+ await onceReportClosed;
+
+ await extension.unload();
+ await closeAboutAddons();
+});
+
+// This test case verifies that the abuse report panel sends the expected data
+// in the "abuse-report:submit" event detail.
+add_task(async function test_abusereport_submit() {
+ // Reset the timestamp of the last report between tests.
+ AbuseReporter._lastReportTimestamp = null;
+ const extension = await installTestExtension();
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id);
+
+ ok(
+ !abuseReportEl._reasonsPanel.hidden,
+ "The list of abuse reasons is the currently visible"
+ );
+
+ let onceUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ abuseReportEl,
+ "submit"
+ );
+ await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext);
+ await onceUpdated;
+
+ is(abuseReportEl.message, "", "The abuse report message is initially empty");
+
+ info("Test typing a message in the abuse report submit panel textarea");
+ const typedMessage = "Description of the extension abuse report";
+
+ EventUtils.synthesizeComposition(
+ {
+ data: typedMessage,
+ type: "compositioncommit",
+ },
+ abuseReportEl.ownerGlobal
+ );
+
+ is(
+ abuseReportEl.message,
+ typedMessage,
+ "Got the expected typed message in the abuse report"
+ );
+
+ const expectedDetail = {
+ addonId: extension.id,
+ };
+
+ const expectedReason = abuseReportEl.reason;
+ const expectedMessage = abuseReportEl.message;
+
+ function handleSubmitRequest({ request, response }) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.write("{}");
+ }
+
+ let reportSubmitted;
+ const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled(
+ ({ data, request, response }) => {
+ reportSubmitted = JSON.parse(data);
+ handleSubmitRequest({ request, response });
+ }
+ );
+
+ const onceReportClosed =
+ AbuseReportTestUtils.promiseReportClosed(abuseReportEl);
+
+ const onMessageBarsCreated = AbuseReportTestUtils.promiseMessageBars(2);
+
+ const onceSubmitEvent = BrowserTestUtils.waitForEvent(
+ abuseReportEl,
+ "abuse-report:submit"
+ );
+ await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnSubmit);
+ const submitEvent = await onceSubmitEvent;
+
+ const actualDetail = {
+ addonId: submitEvent.detail.addonId,
+ };
+ Assert.deepEqual(
+ actualDetail,
+ expectedDetail,
+ "Got the expected detail in the abuse-report:submit event"
+ );
+
+ ok(
+ submitEvent.detail.report,
+ "Got a report object in the abuse-report:submit event detail"
+ );
+
+ // Verify that, when the "abuse-report:submit" has been sent,
+ // the abuse report panel has been hidden, the report has been
+ // submitted and the expected message bar is created in the
+ // HTML about:addons page.
+ info("Wait the report to be submitted to the api server");
+ await onReportSubmitted;
+ info("Wait the report panel to be closed");
+ await onceReportClosed;
+
+ is(
+ reportSubmitted.addon,
+ ADDON_ID,
+ "Got the expected addon in the submitted report"
+ );
+ is(
+ reportSubmitted.reason,
+ expectedReason,
+ "Got the expected reason in the submitted report"
+ );
+ is(
+ reportSubmitted.message,
+ expectedMessage,
+ "Got the expected message in the submitted report"
+ );
+ is(
+ reportSubmitted.report_entry_point,
+ REPORT_ENTRY_POINT,
+ "Got the expected report_entry_point in the submitted report"
+ );
+
+ info("Waiting the expected message bars to be created");
+ const barDetails = await onMessageBarsCreated;
+ is(barDetails.length, 2, "Expect two message bars to have been created");
+ is(
+ barDetails[0].definitionId,
+ "submitting",
+ "Got a submitting message bar as expected"
+ );
+ is(
+ barDetails[1].definitionId,
+ "submitted",
+ "Got a submitted message bar as expected"
+ );
+
+ await extension.unload();
+ await closeAboutAddons();
+});
+
+// This helper does verify that the abuse report panel contains the expected
+// suggestions when the selected reason requires it (and urls are being set
+// on the links elements included in the suggestions when expected).
+async function test_abusereport_suggestions(addonId) {
+ const addon = await AddonManager.getAddonByID(addonId);
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(addonId);
+
+ const {
+ _btnNext,
+ _btnGoBack,
+ _reasonsPanel,
+ _submitPanel,
+ _submitPanel: { _suggestions },
+ } = abuseReportEl;
+
+ for (const reason of AbuseReportTestUtils.getReasons(abuseReportEl)) {
+ const reasonInfo = AbuseReportTestUtils.getReasonInfo(
+ abuseReportEl,
+ reason
+ );
+
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
+ // implementation is also removed.
+ const addonType =
+ addon.type === "sitepermission-deprecated"
+ ? "sitepermission"
+ : addon.type;
+
+ if (reasonInfo.isReasonHidden(addonType)) {
+ continue;
+ }
+
+ info(`Test suggestions for abuse reason "${reason}"`);
+
+ // Select a reason with suggestions.
+ let radioEl = abuseReportEl.querySelector(`#abuse-reason-${reason}`);
+ ok(radioEl, `Found radio button for "${reason}"`);
+ radioEl.checked = true;
+
+ // Make sure the element localization is completed before
+ // checking the content isn't empty.
+ await document.l10n.translateFragment(radioEl);
+
+ // Verify each radio button has a non-empty localized string.
+ const localizedRadioContent = Array.from(
+ radioEl.closest("label").querySelectorAll("[data-l10n-id]")
+ ).filter(el => !el.hidden);
+
+ for (let el of localizedRadioContent) {
+ isnot(
+ el.textContent,
+ "",
+ `Fluent string id '${el.getAttribute("data-l10n-id")}' missing`
+ );
+ }
+
+ // Switch to the submit form with the current reason radio selected.
+ let oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ abuseReportEl,
+ "submit"
+ );
+ await AbuseReportTestUtils.clickPanelButton(_btnNext);
+ await oncePanelUpdated;
+
+ const localizedSuggestionsContent = Array.from(
+ _suggestions.querySelectorAll("[data-l10n-id]")
+ ).filter(el => !el.hidden);
+
+ is(
+ !_suggestions.hidden,
+ !!reasonInfo.hasSuggestions,
+ `Suggestions block has the expected visibility for "${reason}"`
+ );
+ if (reasonInfo.hasSuggestions) {
+ ok(
+ !!localizedSuggestionsContent.length,
+ `Category suggestions should not be empty for "${reason}"`
+ );
+ } else {
+ Assert.strictEqual(
+ localizedSuggestionsContent.length,
+ 0,
+ `Category suggestions should be empty for "${reason}"`
+ );
+ }
+
+ const extSupportLink = _suggestions.querySelector(
+ ".extension-support-link"
+ );
+ if (extSupportLink) {
+ is(
+ extSupportLink.getAttribute("href"),
+ BASE_TEST_MANIFEST.homepage_url,
+ "Got the expected extension-support-url"
+ );
+ }
+
+ const learnMoreLinks = [];
+ learnMoreLinks.push(
+ ..._suggestions.querySelectorAll(
+ 'a[is="moz-support-link"], .abuse-policy-learnmore'
+ )
+ );
+
+ if (learnMoreLinks.length) {
+ is(
+ _suggestions.querySelectorAll(
+ 'a[is="moz-support-link"]:not([support-page])'
+ ).length,
+ 0,
+ "Every SUMO link should point to a specific page"
+ );
+ ok(
+ learnMoreLinks.every(el => el.getAttribute("target") === "_blank"),
+ "All the learn more links have target _blank"
+ );
+ ok(
+ learnMoreLinks.every(el => el.hasAttribute("href")),
+ "All the learn more links have a url set"
+ );
+ }
+
+ oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ abuseReportEl,
+ "reasons"
+ );
+ await AbuseReportTestUtils.clickPanelButton(_btnGoBack);
+ await oncePanelUpdated;
+ ok(!_reasonsPanel.hidden, "Reasons panel should be visible");
+ ok(_submitPanel.hidden, "Submit panel should be hidden");
+ }
+
+ await closeAboutAddons();
+}
+
+add_task(async function test_abusereport_suggestions_extension() {
+ const EXT_ID = "test-extension-suggestions@mochi.test";
+ const extension = await installTestExtension(EXT_ID);
+ await test_abusereport_suggestions(EXT_ID);
+ await extension.unload();
+});
+
+add_task(async function test_abusereport_suggestions_theme() {
+ const THEME_ID = "theme@mochi.test";
+ const theme = await installTestExtension(THEME_ID, "theme");
+ await test_abusereport_suggestions(THEME_ID);
+ await theme.unload();
+});
+
+// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation.
+add_task(async function test_abusereport_suggestions_sitepermission() {
+ const SITEPERM_ADDON_ID = "webmidi@mochi.test";
+ const sitePermAddon = await installTestExtension(
+ SITEPERM_ADDON_ID,
+ "sitepermission-deprecated"
+ );
+ await test_abusereport_suggestions(SITEPERM_ADDON_ID);
+ await sitePermAddon.unload();
+});
+
+// This test case verifies the message bars created on other
+// scenarios (e.g. report creation and submissions errors).
+//
+// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation.
+add_task(async function test_abusereport_messagebars() {
+ const EXT_ID = "test-extension-report@mochi.test";
+ const EXT_ID2 = "test-extension-report-2@mochi.test";
+ const THEME_ID = "test-theme-report@mochi.test";
+ const SITEPERM_ADDON_ID = "webmidi-report@mochi.test";
+ const extension = await installTestExtension(EXT_ID);
+ const extension2 = await installTestExtension(EXT_ID2);
+ const theme = await installTestExtension(THEME_ID, "theme");
+ const sitePermAddon = await installTestExtension(
+ SITEPERM_ADDON_ID,
+ "sitepermission-deprecated"
+ );
+
+ async function assertMessageBars(
+ expectedMessageBarIds,
+ testSetup,
+ testMessageBarDetails
+ ) {
+ await openAboutAddons();
+ const expectedLength = expectedMessageBarIds.length;
+ const onMessageBarsCreated =
+ AbuseReportTestUtils.promiseMessageBars(expectedLength);
+ // Reset the timestamp of the last report between tests.
+ AbuseReporter._lastReportTimestamp = null;
+ await testSetup();
+ info(`Waiting for ${expectedLength} message-bars to be created`);
+ const barDetails = await onMessageBarsCreated;
+ Assert.deepEqual(
+ barDetails.map(d => d.definitionId),
+ expectedMessageBarIds,
+ "Got the expected message bars"
+ );
+ if (testMessageBarDetails) {
+ await testMessageBarDetails(barDetails);
+ }
+ await closeAboutAddons();
+ }
+
+ function setTestRequestHandler(responseStatus, responseData) {
+ AbuseReportTestUtils.promiseReportSubmitHandled(({ request, response }) => {
+ response.setStatusLine(request.httpVersion, responseStatus, "Error");
+ response.write(responseData);
+ });
+ }
+
+ await assertMessageBars(["ERROR_ADDON_NOTFOUND"], async () => {
+ info("Test message bars on addon not found");
+ AbuseReportTestUtils.triggerNewReport(
+ "non-existend-addon-id@mochi.test",
+ REPORT_ENTRY_POINT
+ );
+ });
+
+ await assertMessageBars(["submitting", "ERROR_RECENT_SUBMIT"], async () => {
+ info("Test message bars on recent submission");
+ const promiseRendered = AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT);
+ await promiseRendered;
+ AbuseReporter.updateLastReportTimestamp();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ await assertMessageBars(["submitting", "ERROR_ABORTED_SUBMIT"], async () => {
+ info("Test message bars on aborted submission");
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT);
+ await AbuseReportTestUtils.promiseReportRendered();
+ const { _report } = AbuseReportTestUtils.getReportPanel();
+ _report.abort();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ await assertMessageBars(["submitting", "ERROR_SERVER"], async () => {
+ info("Test message bars on server error");
+ setTestRequestHandler(500);
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT);
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ await assertMessageBars(["submitting", "ERROR_CLIENT"], async () => {
+ info("Test message bars on client error");
+ setTestRequestHandler(400);
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT);
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => {
+ info("Test message bars on unexpected status code");
+ setTestRequestHandler(604);
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT);
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => {
+ info("Test message bars on invalid json in the response data");
+ setTestRequestHandler(200, "");
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT);
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ // Verify message bar on add-on without perm_can_uninstall.
+ await assertMessageBars(
+ ["submitting", "submitted-no-remove-action"],
+ async () => {
+ info("Test message bars on report submitted on an addon without remove");
+ setTestRequestHandler(200, "{}");
+ AbuseReportTestUtils.triggerNewReport(THEME_NO_UNINSTALL_ID, "menu");
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ }
+ );
+
+ // Verify the 3 expected entry points:
+ // menu, toolbar_context_menu and uninstall
+ // (See https://addons-server.readthedocs.io/en/latest/topics/api/abuse.html).
+ await assertMessageBars(["submitting", "submitted"], async () => {
+ info("Test message bars on report opened from addon options menu");
+ setTestRequestHandler(200, "{}");
+ AbuseReportTestUtils.triggerNewReport(EXT_ID, "menu");
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ });
+
+ for (const extId of [EXT_ID, THEME_ID]) {
+ await assertMessageBars(
+ ["submitting", "submitted"],
+ async () => {
+ info(`Test message bars on ${extId} reported from toolbar contextmenu`);
+ setTestRequestHandler(200, "{}");
+ AbuseReportTestUtils.triggerNewReport(extId, "toolbar_context_menu");
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ },
+ ([submittingDetails, submittedDetails]) => {
+ const buttonsL10nId = Array.from(
+ submittedDetails.messagebar.querySelectorAll("button")
+ ).map(el => el.getAttribute("data-l10n-id"));
+ if (extId === THEME_ID) {
+ ok(
+ buttonsL10nId.every(id => id.endsWith("-theme")),
+ "submitted bar actions should use the Fluent id for themes"
+ );
+ } else {
+ ok(
+ buttonsL10nId.every(id => id.endsWith("-extension")),
+ "submitted bar actions should use the Fluent id for extensions"
+ );
+ }
+ }
+ );
+ }
+
+ for (const extId of [EXT_ID2, THEME_ID, SITEPERM_ADDON_ID]) {
+ const testFn = async () => {
+ info(`Test message bars on ${extId} reported opened from addon removal`);
+ setTestRequestHandler(200, "{}");
+ AbuseReportTestUtils.triggerNewReport(extId, "uninstall");
+ await AbuseReportTestUtils.promiseReportRendered();
+ const addon = await AddonManager.getAddonByID(extId);
+ // Ensure that the test extension is pending uninstall as it would be
+ // when a user trigger this scenario on an actual addon uninstall.
+ await addon.uninstall(true);
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ };
+ const assertMessageBarDetails = async ([
+ submittingDetails,
+ submittedDetails,
+ ]) => AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar);
+ await assertMessageBars(
+ ["submitting", "submitted-and-removed"],
+ testFn,
+ assertMessageBarDetails
+ );
+ }
+
+ // Verify message bar on sitepermission add-on type.
+ await assertMessageBars(
+ ["submitting", "submitted"],
+ async () => {
+ info(
+ "Test message bars for report submitted on an sitepermission addon type"
+ );
+ setTestRequestHandler(200, "{}");
+ AbuseReportTestUtils.triggerNewReport(SITEPERM_ADDON_ID, "menu");
+ await AbuseReportTestUtils.promiseReportRendered();
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+ },
+ ([submittingDetails, submittedDetails]) =>
+ AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar)
+ );
+
+ await extension.unload();
+ await extension2.unload();
+ await theme.unload();
+ await sitePermAddon.unload();
+});
+
+add_task(async function test_abusereport_from_aboutaddons_menu() {
+ const EXT_ID = "test-report-from-aboutaddons-menu@mochi.test";
+ const extension = await installTestExtension(EXT_ID);
+
+ await openAboutAddons();
+
+ AbuseReportTestUtils.assertReportPanelHidden();
+
+ const addonCard = gManagerWindow.document.querySelector(
+ `addon-list addon-card[addon-id="${extension.id}"]`
+ );
+ ok(addonCard, "Got the addon-card for the test extension");
+
+ const reportButton = addonCard.querySelector("[action=report]");
+ ok(reportButton, "Got the report action for the test extension");
+
+ info("Click the report action and wait for the 'abuse-report:new' event");
+
+ let onceReportOpened = AbuseReportTestUtils.promiseReportOpened({
+ addonId: extension.id,
+ reportEntryPoint: "menu",
+ });
+ reportButton.click();
+ const panelEl = await onceReportOpened;
+
+ await AbuseReportTestUtils.closeReportPanel(panelEl);
+
+ await closeAboutAddons();
+ await extension.unload();
+});
+
+add_task(async function test_abusereport_from_aboutaddons_remove() {
+ const EXT_ID = "test-report-from-aboutaddons-remove@mochi.test";
+
+ // Test on a theme addon to cover the report checkbox included in the
+ // uninstall dialog also on a theme.
+ const extension = await installTestExtension(EXT_ID, "theme");
+
+ await openAboutAddons("theme");
+
+ AbuseReportTestUtils.assertReportPanelHidden();
+
+ const addonCard = gManagerWindow.document.querySelector(
+ `addon-list addon-card[addon-id="${extension.id}"]`
+ );
+ ok(addonCard, "Got the addon-card for the test theme extension");
+
+ const removeButton = addonCard.querySelector("[action=remove]");
+ ok(removeButton, "Got the remove action for the test theme extension");
+
+ // Prepare the mocked prompt service.
+ const promptService = mockPromptService();
+ promptService.confirmEx = createPromptConfirmEx({
+ remove: true,
+ report: true,
+ });
+
+ info("Click the report action and wait for the 'abuse-report:new' event");
+
+ const onceReportOpened = AbuseReportTestUtils.promiseReportOpened({
+ addonId: extension.id,
+ reportEntryPoint: "uninstall",
+ });
+ removeButton.click();
+ const panelEl = await onceReportOpened;
+
+ await AbuseReportTestUtils.closeReportPanel(panelEl);
+
+ await closeAboutAddons();
+ await extension.unload();
+});
+
+add_task(async function test_abusereport_from_browserAction_remove() {
+ const EXT_ID = "test-report-from-browseraction-remove@mochi.test";
+ const xpiFile = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ ...BASE_TEST_MANIFEST,
+ browser_action: {
+ default_area: "navbar",
+ },
+ browser_specific_settings: { gecko: { id: EXT_ID } },
+ },
+ });
+ const addon = await AddonManager.installTemporaryAddon(xpiFile);
+
+ const buttonId = `${makeWidgetId(EXT_ID)}-browser-action`;
+
+ async function promiseAnimationFrame() {
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ let { tm } = Services;
+ return new Promise(resolve => tm.dispatchToMainThread(resolve));
+ }
+
+ async function reportFromContextMenuRemove() {
+ const menu = document.getElementById("toolbar-context-menu");
+ const node = document.getElementById(CSS.escape(buttonId));
+ const shown = BrowserTestUtils.waitForEvent(
+ menu,
+ "popupshown",
+ "Wair for contextmenu popup"
+ );
+
+ // Wait for an animation frame as we do for the other mochitest-browser
+ // tests related to the browserActions.
+ await promiseAnimationFrame();
+ EventUtils.synthesizeMouseAtCenter(node, { type: "contextmenu" });
+ await shown;
+
+ info(`Clicking on "Remove Extension" context menu item`);
+ let removeExtension = menu.querySelector(
+ ".customize-context-removeExtension"
+ );
+ removeExtension.click();
+
+ return menu;
+ }
+
+ // Prepare the mocked prompt service.
+ const promptService = mockPromptService();
+ promptService.confirmEx = createPromptConfirmEx({
+ remove: true,
+ report: true,
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ info(`Open browserAction context menu in toolbar context menu`);
+ let promiseMenu = reportFromContextMenuRemove();
+
+ // Wait about:addons to be loaded.
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let onceReportOpened = AbuseReportTestUtils.promiseReportOpened({
+ addonId: EXT_ID,
+ reportEntryPoint: "uninstall",
+ managerWindow: browser.contentWindow,
+ });
+
+ is(
+ browser.currentURI.spec,
+ "about:addons",
+ "about:addons tab currently selected"
+ );
+
+ let menu = await promiseMenu;
+ menu.hidePopup();
+
+ let panelEl = await onceReportOpened;
+
+ await AbuseReportTestUtils.closeReportPanel(panelEl);
+
+ let onceExtStarted = AddonTestUtils.promiseWebExtensionStartup(EXT_ID);
+ addon.cancelUninstall();
+ await onceExtStarted;
+
+ // Reload the tab to verify Bug 1559124 didn't regressed.
+ browser.contentWindow.location.reload();
+ await BrowserTestUtils.browserLoaded(browser);
+ is(
+ browser.currentURI.spec,
+ "about:addons",
+ "about:addons tab currently selected"
+ );
+
+ onceReportOpened = AbuseReportTestUtils.promiseReportOpened({
+ addonId: EXT_ID,
+ reportEntryPoint: "uninstall",
+ managerWindow: browser.contentWindow,
+ });
+
+ menu = await reportFromContextMenuRemove();
+ info("Wait for the report panel");
+ panelEl = await onceReportOpened;
+
+ info("Wait for the report panel to be closed");
+ await AbuseReportTestUtils.closeReportPanel(panelEl);
+
+ menu.hidePopup();
+
+ onceExtStarted = AddonTestUtils.promiseWebExtensionStartup(EXT_ID);
+ addon.cancelUninstall();
+ await onceExtStarted;
+ });
+
+ await addon.uninstall();
+});
+
+/*
+ * Test report action hidden on non-supported extension types.
+ */
+add_task(async function test_report_action_hidden_on_builtin_addons() {
+ await openAboutAddons("theme");
+ await AbuseReportTestUtils.assertReportActionHidden(
+ gManagerWindow,
+ DEFAULT_BUILTIN_THEME_ID
+ );
+ await closeAboutAddons();
+});
+
+add_task(async function test_report_action_hidden_on_system_addons() {
+ await openAboutAddons("extension");
+ await AbuseReportTestUtils.assertReportActionHidden(
+ gManagerWindow,
+ EXT_SYSTEM_ADDON_ID
+ );
+ await closeAboutAddons();
+});
+
+add_task(async function test_report_action_hidden_on_dictionary_addons() {
+ await openAboutAddons("dictionary");
+ await AbuseReportTestUtils.assertReportActionHidden(
+ gManagerWindow,
+ EXT_DICTIONARY_ADDON_ID
+ );
+ await closeAboutAddons();
+});
+
+add_task(async function test_report_action_hidden_on_langpack_addons() {
+ await openAboutAddons("locale");
+ await AbuseReportTestUtils.assertReportActionHidden(
+ gManagerWindow,
+ EXT_LANGPACK_ADDON_ID
+ );
+ await closeAboutAddons();
+});
+
+// This test verifies that triggering a report that would be immediately
+// cancelled (e.g. because abuse reports for that extension type are not
+// supported) the abuse report is being hidden as expected.
+add_task(async function test_report_hidden_on_report_unsupported_addontype() {
+ await openAboutAddons();
+
+ let onceCreateReportFailed = AbuseReportTestUtils.promiseMessageBars(1);
+
+ AbuseReportTestUtils.triggerNewReport(EXT_UNSUPPORTED_TYPE_ADDON_ID, "menu");
+
+ await onceCreateReportFailed;
+
+ ok(!AbuseReporter.getOpenDialog(), "report dialog should not be open");
+
+ await closeAboutAddons();
+});
+
+/*
+ * Test regression fixes.
+ */
+
+add_task(async function test_no_broken_suggestion_on_missing_supportURL() {
+ const EXT_ID = "test-no-author@mochi.test";
+ const extension = await installTestExtension(EXT_ID, "extension", {
+ homepage_url: undefined,
+ });
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(EXT_ID);
+
+ info("Select broken as the abuse reason");
+ abuseReportEl.querySelector("#abuse-reason-broken").checked = true;
+
+ let oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ abuseReportEl,
+ "submit"
+ );
+ await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext);
+ await oncePanelUpdated;
+
+ const suggestionEl = abuseReportEl.querySelector(
+ "abuse-report-reason-suggestions"
+ );
+ is(suggestionEl.reason, "broken", "Got the expected suggestion element");
+ ok(suggestionEl.hidden, "suggestion element should be empty");
+
+ await closeAboutAddons();
+ await extension.unload();
+});
+
+// This test verify that the abuse report panel is opening the
+// author link using a null triggeringPrincipal.
+add_task(async function test_abusereport_open_author_url() {
+ const abuseReportEl = await AbuseReportTestUtils.openReport(
+ EXT_WITH_PRIVILEGED_URL_ID
+ );
+
+ const authorLink = abuseReportEl._linkAddonAuthor;
+ ok(authorLink, "Got the author link element");
+ is(
+ authorLink.href,
+ "about:config",
+ "Got a privileged url in the link element"
+ );
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ // eslint-disable-next-line max-len
+ /Security Error: Content at moz-nullprincipal:{.*} may not load or link to about:config/,
+ },
+ ]);
+ });
+
+ let tabSwitched = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone");
+ authorLink.click();
+ await tabSwitched;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Got about:blank loaded in the new tab"
+ );
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await closeAboutAddons();
+});
+
+add_task(async function test_no_report_checkbox_for_unsupported_addon_types() {
+ async function test_report_checkbox_hidden(addon) {
+ await openAboutAddons(addon.type);
+
+ const addonCard = gManagerWindow.document.querySelector(
+ `addon-list addon-card[addon-id="${addon.id}"]`
+ );
+ ok(addonCard, "Got the addon-card for the test extension");
+
+ const removeButton = addonCard.querySelector("[action=remove]");
+ ok(removeButton, "Got the remove action for the test extension");
+
+ // Prepare the mocked prompt service.
+ const promptService = mockPromptService();
+ promptService.confirmEx = createPromptConfirmEx({
+ remove: true,
+ report: false,
+ expectCheckboxHidden: true,
+ });
+
+ info("Click the report action and wait for the addon to be removed");
+ const promiseCardRemoved = BrowserTestUtils.waitForEvent(
+ addonCard.closest("addon-list"),
+ "remove"
+ );
+ removeButton.click();
+ await promiseCardRemoved;
+
+ await closeAboutAddons();
+ }
+
+ const reportNotSupportedAddons = [
+ {
+ id: "fake-langpack-to-remove@mochi.test",
+ name: "This is a fake langpack",
+ version: "1.1",
+ type: "locale",
+ },
+ {
+ id: "fake-dictionary-to-remove@mochi.test",
+ name: "This is a fake dictionary",
+ version: "1.1",
+ type: "dictionary",
+ },
+ ];
+
+ AbuseReportTestUtils.createMockAddons(reportNotSupportedAddons);
+
+ for (const { id } of reportNotSupportedAddons) {
+ const addon = await AddonManager.getAddonByID(id);
+ await test_report_checkbox_hidden(addon);
+ }
+});
+
+add_task(async function test_author_hidden_when_missing() {
+ const EXT_ID = "test-no-author@mochi.test";
+ const extension = await installTestExtension(EXT_ID, "extension", {
+ author: undefined,
+ });
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(EXT_ID);
+
+ const addon = await AddonManager.getAddonByID(EXT_ID);
+
+ ok(!addon.creator, "addon.creator should not be undefined");
+ ok(
+ abuseReportEl._addonAuthorContainer.hidden,
+ "author container should be hidden"
+ );
+
+ await closeAboutAddons();
+ await extension.unload();
+});
+
+// Verify addon.siteOrigin is used as a fallback when homepage_url/developer.url
+// or support url are missing.
+//
+// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation.
+add_task(async function test_siteperm_siteorigin_fallback() {
+ const SITEPERM_ADDON_ID = "webmidi-site-origin@mochi.test";
+ const sitePermAddon = await installTestExtension(
+ SITEPERM_ADDON_ID,
+ "sitepermission-deprecated",
+ {
+ homepage_url: undefined,
+ }
+ );
+
+ const abuseReportEl = await AbuseReportTestUtils.openReport(
+ SITEPERM_ADDON_ID
+ );
+ const addon = await AddonManager.getAddonByID(SITEPERM_ADDON_ID);
+
+ ok(addon.siteOrigin, "addon.siteOrigin should not be undefined");
+ ok(!addon.supportURL, "addon.supportURL should not be set");
+ ok(!addon.homepageURL, "addon.homepageURL should not be set");
+ is(
+ abuseReportEl.supportURL,
+ addon.siteOrigin,
+ "Got the expected support_url"
+ );
+
+ await closeAboutAddons();
+ await sitePermAddon.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js
new file mode 100644
index 0000000000..1efb28add3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+loadTestSubscript("head_abuse_report.js");
+
+add_setup(async function () {
+ // Make sure the integrated abuse report panel is the one enabled
+ // while this test file runs (instead of the AMO hosted form).
+ // NOTE: behaviors expected when amoFormEnabled is true are tested
+ // in the separate browser_amo_abuse_report.js test file.
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amoFormEnabled", false]],
+ });
+ await AbuseReportTestUtils.setup();
+});
+
+/**
+ * Test tasks specific to the abuse report opened in its own dialog window.
+ */
+
+add_task(async function test_close_icon_button_hidden_when_dialog() {
+ const addonId = "addon-to-report@mochi.test";
+ const extension = await installTestExtension(addonId);
+
+ const reportDialog = await AbuseReporter.openDialog(
+ addonId,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+ await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ const panelEl = await reportDialog.promiseReportPanel;
+
+ let promiseClosedWindow = waitClosedWindow();
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, panelEl.ownerGlobal);
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+
+ await promiseClosedWindow;
+ ok(
+ await reportDialog.promiseReport,
+ "expect the report to not be cancelled by pressing enter"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_report_triggered_when_report_dialog_is_open() {
+ const addonId = "addon-to-report@mochi.test";
+ const extension = await installTestExtension(addonId);
+
+ const reportDialog = await AbuseReporter.openDialog(
+ addonId,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+ await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ let promiseClosedWindow = waitClosedWindow();
+
+ const reportDialog2 = await AbuseReporter.openDialog(
+ addonId,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+
+ await promiseClosedWindow;
+
+ // Trigger the report submit and check that the second report is
+ // resolved as expected.
+ await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ ok(
+ !reportDialog.window || reportDialog.window.closed,
+ "expect the first dialog to be closed"
+ );
+ ok(!!reportDialog2.window, "expect the second dialog to be open");
+
+ is(
+ reportDialog2.window,
+ AbuseReportTestUtils.getReportDialog(),
+ "Got a report dialog as expected"
+ );
+
+ AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
+
+ // promiseReport is resolved to undefined if the report has been
+ // cancelled, otherwise it is resolved to a report object.
+ ok(
+ !(await reportDialog.promiseReport),
+ "expect the first report to be cancelled"
+ );
+ ok(
+ !!(await reportDialog2.promiseReport),
+ "expect the second report to be resolved"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_report_dialog_window_closed_by_user() {
+ const addonId = "addon-to-report@mochi.test";
+ const extension = await installTestExtension(addonId);
+
+ const reportDialog = await AbuseReporter.openDialog(
+ addonId,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+ await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ let promiseClosedWindow = waitClosedWindow();
+
+ reportDialog.close();
+
+ await promiseClosedWindow;
+
+ ok(
+ !(await reportDialog.promiseReport),
+ "expect promiseReport to be resolved as user cancelled"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_amo_details_for_not_installed_addon() {
+ const addonId = "not-installed-addon@mochi.test";
+ const fakeAMODetails = {
+ name: "fake name",
+ current_version: { version: "1.0" },
+ type: "extension",
+ icon_url: "http://test.addons.org/asserts/fake-icon-url.png",
+ homepage: "http://fake.url/homepage",
+ support_url: "http://fake.url/support",
+ authors: [
+ { name: "author1", url: "http://fake.url/author1" },
+ { name: "author2", url: "http://fake.url/author2" },
+ ],
+ is_recommended: true,
+ };
+
+ AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
+ registerCleanupFunction(() =>
+ AbuseReportTestUtils.amoAddonDetailsMap.clear()
+ );
+
+ const reportDialog = await AbuseReporter.openDialog(
+ addonId,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+
+ const reportEl = await reportDialog.promiseReportPanel;
+
+ // Assert that the panel has been able to retrieve from AMO
+ // all the addon details needed to render the panel correctly.
+ is(reportEl.addonId, addonId, "Got the expected addonId");
+ is(reportEl.addonName, fakeAMODetails.name, "Got the expected addon name");
+ is(reportEl.addonType, fakeAMODetails.type, "Got the expected addon type");
+ is(
+ reportEl.authorName,
+ fakeAMODetails.authors[0].name,
+ "Got the first author name as expected"
+ );
+ is(
+ reportEl.authorURL,
+ fakeAMODetails.authors[0].url,
+ "Got the first author url as expected"
+ );
+ is(reportEl.iconURL, fakeAMODetails.icon_url, "Got the expected icon url");
+ is(
+ reportEl.supportURL,
+ fakeAMODetails.support_url,
+ "Got the expected support url"
+ );
+ is(
+ reportEl.homepageURL,
+ fakeAMODetails.homepage,
+ "Got the expected homepage url"
+ );
+
+ reportDialog.close();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js
new file mode 100644
index 0000000000..939fe421c3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js
@@ -0,0 +1,827 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+const { PERMISSION_L10N, PERMISSION_L10N_ID_OVERRIDES } =
+ ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissionMessages.sys.mjs"
+ );
+
+AddonTestUtils.initMochitest(this);
+
+async function background() {
+ browser.permissions.onAdded.addListener(perms => {
+ browser.test.sendMessage("permission-added", perms);
+ });
+ browser.permissions.onRemoved.addListener(perms => {
+ browser.test.sendMessage("permission-removed", perms);
+ });
+}
+
+async function getExtensions({ manifest_version = 2 } = {}) {
+ let extensions = {
+ "addon0@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 0",
+ browser_specific_settings: { gecko: { id: "addon0@mochi.test" } },
+ permissions: ["alarms", "contextMenus"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon1@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 1",
+ browser_specific_settings: { gecko: { id: "addon1@mochi.test" } },
+ permissions: ["alarms", "contextMenus", "tabs", "webNavigation"],
+ // Note: for easier testing, we merge host_permissions into permissions
+ // when loading mv2 extensions, see ExtensionTestCommon.generateFiles.
+ host_permissions: ["<all_urls>", "file://*/*"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon2@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 2",
+ browser_specific_settings: { gecko: { id: "addon2@mochi.test" } },
+ permissions: ["alarms", "contextMenus"],
+ optional_permissions: ["http://mochi.test/*"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon3@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 3",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "addon3@mochi.test" } },
+ permissions: ["tabs"],
+ optional_permissions: ["webNavigation", "<all_urls>"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon4@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 4",
+ browser_specific_settings: { gecko: { id: "addon4@mochi.test" } },
+ optional_permissions: ["tabs", "webNavigation"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon5@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 5",
+ browser_specific_settings: { gecko: { id: "addon5@mochi.test" } },
+ optional_permissions: ["*://*/*"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "priv6@mochi.test": ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ manifest_version,
+ name: "Privileged add-on 6",
+ browser_specific_settings: { gecko: { id: "priv6@mochi.test" } },
+ optional_permissions: [
+ "file://*/*",
+ "about:reader*",
+ "resource://pdf.js/*",
+ "*://*.mozilla.com/*",
+ "*://*/*",
+ "<all_urls>",
+ ],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon7@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 7",
+ browser_specific_settings: { gecko: { id: "addon7@mochi.test" } },
+ optional_permissions: ["<all_urls>", "https://*/*", "file://*/*"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "addon8@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 8",
+ browser_specific_settings: { gecko: { id: "addon8@mochi.test" } },
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ optional_permissions: ["https://*/*", "http://*/*", "file://*/*"],
+ },
+ background,
+ useAddonManager: "temporary",
+ }),
+ "other@mochi.test": ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ name: "Test add-on 6",
+ browser_specific_settings: { gecko: { id: "other@mochi.test" } },
+ optional_permissions: [
+ "tabs",
+ "webNavigation",
+ "<all_urls>",
+ "*://*/*",
+ ],
+ },
+ useAddonManager: "temporary",
+ }),
+ };
+ for (let ext of Object.values(extensions)) {
+ await ext.startup();
+ }
+ return extensions;
+}
+
+async function runTest(options) {
+ let {
+ extension,
+ addonId,
+ permissions = [],
+ optional_permissions = [],
+ optional_overlapping = [],
+ optional_enabled = [],
+ // Map<permission->string> to check optional_permissions against, if set.
+ optional_strings = {},
+ view,
+ } = options;
+ if (extension) {
+ addonId = extension.id;
+ }
+
+ let win = view || (await loadInitialView("extension"));
+
+ let card = getAddonCard(win, addonId);
+ let permsSection = card.querySelector("addon-permissions-list");
+ if (!permsSection) {
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+ }
+
+ card = getAddonCard(win, addonId);
+ let { deck, tabGroup } = card.details;
+
+ let permsBtn = tabGroup.querySelector('[name="permissions"]');
+ let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ permsBtn.click();
+ await permsShown;
+
+ permsSection = card.querySelector("addon-permissions-list");
+
+ let rows = Array.from(permsSection.querySelectorAll(".addon-detail-row"));
+ let permission_rows = Array.from(
+ permsSection.querySelectorAll(".permission-info")
+ );
+
+ // Last row is the learn more link.
+ info("Check learn more link");
+ let link = rows[rows.length - 1].firstElementChild;
+ let rootUrl = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = rootUrl + "extension-permissions";
+ is(link.href, url, "The URL is set");
+ is(link.getAttribute("target"), "_blank", "The link opens in a new tab");
+
+ // We should have one more row (learn more) that the combined permissions,
+ // or if no permissions, 2 rows.
+ let num_permissions = permissions.length + optional_permissions.length;
+ is(
+ permission_rows.length,
+ num_permissions,
+ "correct number of details rows are present"
+ );
+
+ info("Check displayed permissions");
+ if (!num_permissions) {
+ is(
+ win.document.l10n.getAttributes(rows[0]).id,
+ "addon-permissions-empty",
+ "There's a message when no permissions are shown"
+ );
+ }
+ if (permissions.length) {
+ for (let name of permissions) {
+ // Check the permission-info class to make sure it's for a permission.
+ let row = permission_rows.shift();
+ ok(
+ row.classList.contains("permission-info"),
+ `required permission row for ${name}`
+ );
+ }
+ }
+
+ let addon = await AddonManager.getAddonByID(addonId);
+ info(`addon ${addon.id} is ${addon.userDisabled ? "disabled" : "enabled"}`);
+
+ function waitForPermissionChange(id) {
+ return new Promise(resolve => {
+ info(`listening for change on ${id}`);
+ let listener = (type, data) => {
+ info(`change permissions ${JSON.stringify(data)}`);
+ if (data.extensionId !== id) {
+ return;
+ }
+ ExtensionPermissions.removeListener(listener);
+ resolve(data);
+ };
+ ExtensionPermissions.addListener(listener);
+ });
+ }
+
+ // This tests the permission change and button state when the user
+ // changes the state in about:addons.
+ async function testTogglePermissionButton(
+ permissions,
+ button,
+ excpectDisabled = false
+ ) {
+ let enabled = permissions.some(perm => optional_enabled.includes(perm));
+ if (excpectDisabled) {
+ enabled = !enabled;
+ }
+ is(
+ button.pressed,
+ enabled,
+ `permission is set correctly for ${permissions}: ${button.pressed}`
+ );
+ let change;
+ if (addon.userDisabled || !extension) {
+ change = waitForPermissionChange(addonId);
+ } else if (!enabled) {
+ change = extension.awaitMessage("permission-added");
+ } else {
+ change = extension.awaitMessage("permission-removed");
+ }
+
+ button.click();
+
+ let perms = await change;
+ if (addon.userDisabled || !extension) {
+ perms = enabled ? perms.removed : perms.added;
+ }
+
+ Assert.greater(
+ perms.permissions.length + perms.origins.length,
+ 0,
+ "Some permission(s) toggled."
+ );
+
+ if (perms.permissions.length) {
+ // Only check api permissions against the first passed permission,
+ // because we treat <all_urls> as an api permission, but not *://*/*.
+ is(perms.permissions.length, 1, "A single api permission toggled.");
+ is(perms.permissions[0], permissions[0], "Correct api permission.");
+ }
+ if (perms.origins.length) {
+ Assert.deepEqual(
+ perms.origins.slice().sort(),
+ permissions.slice().sort(),
+ "Toggled origin permission."
+ );
+ }
+
+ await BrowserTestUtils.waitForCondition(async () => {
+ return button.pressed == !enabled;
+ }, "button changed state");
+ }
+
+ // This tests that the button changes state if the permission is
+ // changed outside of about:addons
+ async function testExternalPermissionChange(permission, button) {
+ let enabled = button.pressed;
+ let type = button.getAttribute("permission-type");
+ let change;
+ if (addon.userDisabled || !extension) {
+ change = waitForPermissionChange(addonId);
+ } else if (!enabled) {
+ change = extension.awaitMessage("permission-added");
+ } else {
+ change = extension.awaitMessage("permission-removed");
+ }
+
+ let permissions = { permissions: [], origins: [] };
+ if (type == "origin") {
+ permissions.origins = [permission];
+ } else {
+ permissions.permissions = [permission];
+ }
+
+ if (enabled) {
+ await ExtensionPermissions.remove(addonId, permissions);
+ } else {
+ await ExtensionPermissions.add(addonId, permissions);
+ }
+
+ let perms = await change;
+ if (addon.userDisabled || !extension) {
+ perms = enabled ? perms.removed : perms.added;
+ }
+ ok(
+ perms.permissions.includes(permission) ||
+ perms.origins.includes(permission),
+ "permission was toggled"
+ );
+
+ await BrowserTestUtils.waitForCondition(async () => {
+ return button.pressed == !enabled;
+ }, "button changed state");
+ }
+
+ // This tests that changing the permission on another addon does
+ // not change the UI for the addon we're testing.
+ async function testOtherPermissionChange(permission, toggle) {
+ let type = toggle.getAttribute("permission-type");
+ let otherId = "other@mochi.test";
+ let change = waitForPermissionChange(otherId);
+ let perms = await ExtensionPermissions.get(otherId);
+ let existing = type == "origin" ? perms.origins : perms.permissions;
+ let permissions = { permissions: [], origins: [] };
+ if (type == "origin") {
+ permissions.origins = [permission];
+ } else {
+ permissions.permissions = [permission];
+ }
+
+ if (existing.includes(permission)) {
+ await ExtensionPermissions.remove(otherId, permissions);
+ } else {
+ await ExtensionPermissions.add(otherId, permissions);
+ }
+ await change;
+ }
+
+ if (optional_permissions.length) {
+ for (let name of optional_permissions) {
+ // Set of permissions represented by this key.
+ let perms = [name];
+ if (name === optional_overlapping[0]) {
+ perms = optional_overlapping;
+ }
+
+ // Check the row is a permission row with the correct key on the toggle
+ // control.
+ let row = permission_rows.shift();
+ let toggle = row.querySelector("moz-toggle");
+ let label = toggle.labelEl;
+
+ let str = optional_strings[name];
+ if (str) {
+ is(label.textContent.trim(), str, `Expected permission string ${str}`);
+ }
+
+ ok(
+ row.classList.contains("permission-info"),
+ `optional permission row for ${name}`
+ );
+ is(
+ toggle.getAttribute("permission-key"),
+ name,
+ `optional permission toggle exists for ${name}`
+ );
+
+ await testTogglePermissionButton(perms, toggle);
+ await testTogglePermissionButton(perms, toggle, true);
+
+ for (let perm of perms) {
+ // make a change "outside" the UI and check the values.
+ // toggle twice to test both add/remove.
+ await testExternalPermissionChange(perm, toggle);
+ // change another addon to mess around with optional permission
+ // values to see if it effects the addon we're testing here. The
+ // next check would fail if anything bleeds onto other addons.
+ await testOtherPermissionChange(perm, toggle);
+ // repeat the "outside" test.
+ await testExternalPermissionChange(perm, toggle);
+ }
+ }
+ }
+
+ if (!view) {
+ await closeView(win);
+ }
+}
+
+async function testPermissionsView({ manifestV3enabled, manifest_version }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", manifestV3enabled]],
+ });
+
+ // pre-set a permission prior to starting extensions.
+ await ExtensionPermissions.add("addon4@mochi.test", {
+ permissions: ["tabs"],
+ origins: [],
+ });
+
+ let extensions = await getExtensions({ manifest_version });
+
+ info("Check add-on with required permissions");
+ if (manifest_version < 3) {
+ await runTest({
+ extension: extensions["addon1@mochi.test"],
+ permissions: ["<all_urls>", "tabs", "webNavigation"],
+ });
+ } else {
+ await runTest({
+ extension: extensions["addon1@mochi.test"],
+ permissions: ["tabs", "webNavigation"],
+ optional_permissions: ["<all_urls>"],
+ });
+ }
+
+ info("Check add-on without any displayable permissions");
+ await runTest({ extension: extensions["addon0@mochi.test"] });
+
+ info("Check add-on with only one optional origin.");
+ await runTest({
+ extension: extensions["addon2@mochi.test"],
+ optional_permissions: manifestV3enabled ? ["http://mochi.test/*"] : [],
+ optional_strings: {
+ "http://mochi.test/*": "Access your data for http://mochi.test",
+ },
+ });
+
+ info("Check add-on with both required and optional permissions");
+ await runTest({
+ extension: extensions["addon3@mochi.test"],
+ permissions: ["tabs"],
+ optional_permissions: ["webNavigation", "<all_urls>"],
+ });
+
+ // Grant a specific optional host permission not listed in the manifest.
+ await ExtensionPermissions.add("addon3@mochi.test", {
+ permissions: [],
+ origins: ["https://example.com/*"],
+ });
+ await extensions["addon3@mochi.test"].awaitMessage("permission-added");
+
+ info("Check addon3 again and expect the new optional host permission");
+ await runTest({
+ extension: extensions["addon3@mochi.test"],
+ permissions: ["tabs"],
+ optional_permissions: [
+ "webNavigation",
+ "<all_urls>",
+ ...(manifestV3enabled ? ["https://example.com/*"] : []),
+ ],
+ optional_enabled: ["https://example.com/*"],
+ optional_strings: {
+ "https://example.com/*": "Access your data for https://example.com",
+ },
+ });
+
+ info("Check add-on with only optional permissions, tabs is pre-enabled");
+ await runTest({
+ extension: extensions["addon4@mochi.test"],
+ optional_permissions: ["tabs", "webNavigation"],
+ optional_enabled: ["tabs"],
+ });
+
+ info("Check add-on with a global match pattern in place of all urls");
+ await runTest({
+ extension: extensions["addon5@mochi.test"],
+ optional_permissions: ["*://*/*"],
+ });
+
+ info("Check privileged add-on with non-web origin permissions");
+ await runTest({
+ extension: extensions["priv6@mochi.test"],
+ optional_permissions: [
+ "<all_urls>",
+ ...(manifestV3enabled ? ["*://*.mozilla.com/*"] : []),
+ ],
+ optional_overlapping: ["<all_urls>", "*://*/*"],
+ optional_strings: {
+ "*://*.mozilla.com/*":
+ "Access your data for sites in the *://mozilla.com domain",
+ },
+ });
+
+ info(`Check that <all_urls> is used over other "all websites" permissions`);
+ await runTest({
+ extension: extensions["addon7@mochi.test"],
+ optional_permissions: ["<all_urls>"],
+ optional_overlapping: ["<all_urls>", "https://*/*"],
+ });
+
+ info(`Also check different "all sites" permissions in the manifest`);
+ await runTest({
+ extension: extensions["addon8@mochi.test"],
+ optional_permissions: ["https://*/*"],
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ optional_overlapping: ["https://*/*", "http://*/*"],
+ });
+
+ for (let ext of Object.values(extensions)) {
+ await ext.unload();
+ }
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function testPermissionsView_MV2_manifestV3disabled() {
+ await testPermissionsView({ manifestV3enabled: false, manifest_version: 2 });
+});
+
+add_task(async function testPermissionsView_MV2_manifestV3enabled() {
+ await testPermissionsView({ manifestV3enabled: true, manifest_version: 2 });
+});
+
+add_task(async function testPermissionsView_MV3() {
+ await testPermissionsView({ manifestV3enabled: true, manifest_version: 3 });
+});
+
+add_task(async function testPermissionsViewStates() {
+ let ID = "addon@mochi.test";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test add-on 3",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["tabs"],
+ optional_permissions: ["webNavigation", "<all_urls>"],
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ info(
+ "Check toggling permissions on a disabled addon with addon3@mochi.test."
+ );
+ let view = await loadInitialView("extension");
+ let addon = await AddonManager.getAddonByID(ID);
+ await addon.disable();
+ ok(addon.userDisabled, "addon is disabled");
+ await runTest({
+ extension,
+ permissions: ["tabs"],
+ optional_permissions: ["webNavigation", "<all_urls>"],
+ view,
+ });
+ await addon.enable();
+ ok(!addon.userDisabled, "addon is enabled");
+
+ async function install_addon(extensionData) {
+ let xpi = await AddonTestUtils.createTempWebExtensionFile(extensionData);
+ let { addon } = await AddonTestUtils.promiseInstallFile(xpi);
+ return addon;
+ }
+
+ function wait_for_addon_item_updated(addonId) {
+ return BrowserTestUtils.waitForEvent(getAddonCard(view, addonId), "update");
+ }
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ addon = await install_addon({
+ manifest: {
+ name: "Test add-on 3",
+ version: "2.0",
+ browser_specific_settings: { gecko: { id: ID } },
+ optional_permissions: ["webNavigation"],
+ },
+ useAddonManager: "permanent",
+ });
+ is(addon.version, "2.0", "addon upgraded");
+ await promiseItemUpdated;
+
+ await runTest({
+ addonId: addon.id,
+ optional_permissions: ["webNavigation"],
+ view,
+ });
+
+ // While the view is still available, test setting a permission
+ // that is not in the manifest of the addon.
+ let card = getAddonCard(view, addon.id);
+ await Assert.rejects(
+ card.setAddonPermission("webRequest", "permission", "add"),
+ /permission missing from manifest/,
+ "unable to set the addon permission"
+ );
+
+ await closeView(view);
+ await extension.unload();
+});
+
+add_task(async function testAllUrlsNotGrantedUnconditionally_MV3() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.manifestV3.enabled", true]],
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["<all_urls>"],
+ },
+ async background() {
+ const perms = await browser.permissions.getAll();
+ browser.test.sendMessage("granted-permissions", perms);
+ },
+ });
+
+ await extension.startup();
+ const perms = await extension.awaitMessage("granted-permissions");
+ ok(
+ !perms.origins.includes("<all_urls>"),
+ "Optional <all_urls> should not be granted as host permission yet"
+ );
+ ok(
+ !perms.permissions.includes("<all_urls>"),
+ "Optional <all_urls> should not be granted as an API permission neither"
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_OneOfMany_AllSites_toggle() {
+ // ESLint autofix will silently convert http://*/* match patterns into https.
+ /* eslint-disable @microsoft/sdl/no-insecure-url */
+ let id = "addon9@mochi.test";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test add-on 9",
+ browser_specific_settings: { gecko: { id } },
+ optional_permissions: ["http://*/*", "https://*/*"],
+ },
+ background,
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ // Grant the second "all sites" permission as listed in the manifest.
+ await ExtensionPermissions.add("addon9@mochi.test", {
+ permissions: [],
+ origins: ["https://*/*"],
+ });
+ await extension.awaitMessage("permission-added");
+
+ let view = await loadInitialView("extension");
+ let addon = await AddonManager.getAddonByID(id);
+
+ let card = getAddonCard(view, addon.id);
+
+ let permsSection = card.querySelector("addon-permissions-list");
+ if (!permsSection) {
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+ let loaded = waitForViewLoad(view);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+ }
+
+ card = getAddonCard(view, addon.id);
+ let { deck, tabGroup } = card.details;
+
+ let permsBtn = tabGroup.querySelector('[name="permissions"]');
+ let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ permsBtn.click();
+ await permsShown;
+
+ permsSection = card.querySelector("addon-permissions-list");
+ let permission_rows = permsSection.querySelectorAll(".permission-info");
+ is(permission_rows.length, 1, "Only one 'all sites' permission toggle.");
+
+ let row = permission_rows[0];
+ let toggle = row.querySelector("moz-toggle");
+ ok(
+ row.classList.contains("permission-info"),
+ `optional permission row for "http://*/*"`
+ );
+ is(
+ toggle.getAttribute("permission-key"),
+ "http://*/*",
+ `optional permission toggle exists for "http://*/*"`
+ );
+ ok(toggle.pressed, "Expect 'all sites' toggle to be set.");
+
+ // Revoke the second "all sites" permission, expect toggle to be unchecked.
+ await ExtensionPermissions.remove("addon9@mochi.test", {
+ permissions: [],
+ origins: ["https://*/*"],
+ });
+ await extension.awaitMessage("permission-removed");
+ ok(!toggle.pressed, "Expect 'all sites' toggle not to be pressed.");
+
+ toggle.click();
+
+ let granted = await extension.awaitMessage("permission-added");
+ Assert.deepEqual(granted, {
+ permissions: [],
+ origins: ["http://*/*", "https://*/*"],
+ });
+
+ await closeView(view);
+ await extension.unload();
+ /* eslint-enable @microsoft/sdl/no-insecure-url */
+});
+
+add_task(async function testOverrideLocalization() {
+ // Mock a fluent file.
+ const l10nReg = L10nRegistry.getInstance();
+ const source = L10nFileSource.createMock(
+ "mock",
+ "app",
+ ["en-US"],
+ "/localization/",
+ [
+ {
+ path: "/localization/mock.ftl",
+ source: `
+webext-perms-description-test-tabs = Custom description for the tabs permission
+`,
+ },
+ ]
+ );
+ l10nReg.registerSources([source]);
+
+ // Add the mocked fluent file to PERMISSION_L10N and override the tabs
+ // permission to use the alternative string. In a real world use-case, this
+ // would be used to add non-toolkit fluent files with permission strings of
+ // APIs which are defined outside of toolkit.
+ PERMISSION_L10N.addResourceIds(["mock.ftl"]);
+ PERMISSION_L10N_ID_OVERRIDES.set(
+ "tabs",
+ "webext-perms-description-test-tabs"
+ );
+
+ let mockCleanup = () => {
+ // Make sure cleanup is executed only once.
+ mockCleanup = () => {};
+
+ // Remove the non-toolkit permission string.
+ PERMISSION_L10N.removeResourceIds(["mock.ftl"]);
+ PERMISSION_L10N_ID_OVERRIDES.delete("tabs");
+ l10nReg.removeSources(["mock"]);
+ };
+ registerCleanupFunction(mockCleanup);
+
+ // Load an example add-on which uses the tabs permission.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 2,
+ name: "Simple test add-on",
+ browser_specific_settings: { gecko: { id: "testAddon@mochi.test" } },
+ permissions: ["tabs"],
+ },
+ background,
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let addonId = extension.id;
+
+ let win = await loadInitialView("extension");
+
+ // Open the card and navigate to its permission list.
+ let card = getAddonCard(win, addonId);
+ let permsSection = card.querySelector("addon-permissions-list");
+ if (!permsSection) {
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+ }
+
+ card = getAddonCard(win, addonId);
+ let { deck, tabGroup } = card.details;
+
+ let permsBtn = tabGroup.querySelector('[name="permissions"]');
+ let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ permsBtn.click();
+ await permsShown;
+ let permissionList = card.querySelector("addon-permissions-list");
+ let permissionEntries = Array.from(permissionList.querySelectorAll("li"));
+ Assert.equal(
+ permissionEntries.length,
+ 1,
+ "Should find a single permission entry"
+ );
+ Assert.equal(
+ permissionEntries[0].textContent,
+ "Custom description for the tabs permission",
+ "Should find the non-default permission description"
+ );
+
+ await closeView(win);
+ await extension.unload();
+
+ mockCleanup();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
new file mode 100644
index 0000000000..76e7f2b255
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
@@ -0,0 +1,1675 @@
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const { QuarantinedDomains } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const SUPPORT_URL = Services.urlFormatter.formatURL(
+ Services.prefs.getStringPref("app.support.baseURL")
+);
+const PB_SUMO_URL = SUPPORT_URL + "extensions-pb";
+const DEFAULT_THEME_ID = "default-theme@mozilla.org";
+const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
+
+let gProvider;
+let promptService;
+
+AddonTestUtils.initMochitest(this);
+
+function getDetailRows(card) {
+ return Array.from(
+ card.querySelectorAll('[name="details"] .addon-detail-row:not([hidden])')
+ );
+}
+
+async function checkLabel(row, name) {
+ let id;
+ if (name == "private-browsing") {
+ // This id is carried over from the old about:addons.
+ id = "detail-private-browsing-label";
+ } else {
+ id = `addon-detail-${name}-label`;
+ }
+ const doc = row.ownerDocument;
+ await doc.l10n.translateElements([row]);
+ const rowHeaderEl = row.firstElementChild;
+ is(doc.l10n.getAttributes(rowHeaderEl).id, id, `The ${name} label is set`);
+ if (row.getAttribute("role") === "group") {
+ // For the rows on which the role="group" attribute is set,
+ // let's make sure that the element itself includes an aria-label
+ // which provides to the screen reader a label similar to the one
+ // rendered as the visual section header.
+ //
+ // NOTE: more screen reader accessibility assertions are being
+ // covered by the checkRowScreenReaderAccessibility test helper.
+ is(
+ row.getAttribute("aria-label"),
+ rowHeaderEl.textContent,
+ "expect an aria-label from role=group row to match row header el text"
+ );
+ // For these rows we expect rowHeaderEl to be a span.
+ is(rowHeaderEl.tagName, "SPAN", "row header element should be a span");
+ } else {
+ // For the other rows which we have not set a role="group" attribute
+ // on, we expect the rowHeaderEl to still be a label.
+ is(
+ rowHeaderEl.tagName,
+ "LABEL",
+ "row header element expected to be a label"
+ );
+ }
+}
+
+async function checkRowScreenReaderAccessibility(
+ row,
+ { groupName, expectedFluentId }
+) {
+ const doc = row.ownerDocument;
+ // Make sure the row isn't missing any strings expected to be associated
+ // to the fluent ids (which would make translateElements to reject
+ // and the test to fail explicitly).
+ await doc.l10n.translateElements([row]);
+ is(
+ row.getAttribute("role"),
+ "group",
+ `Expect ${groupName} row to have role group`
+ );
+ is(
+ doc.l10n.getAttributes(row).id,
+ expectedFluentId,
+ `Got expected fluent id associated to the ${groupName} row`
+ );
+ // Make sure that screen readers will be able to announce to the
+ // user what is the group of controls being entered.
+ ok(
+ !!row.getAttribute("aria-label")?.length,
+ `Expect non empty aria-label on the ${groupName} row`
+ );
+}
+
+async function checkQuarantinedDomainsUserAllowedRows(card, rows) {
+ // Account for the rows related to per-addon quarantineIgnoredByUser UI,
+ // underling functionality of the UI is checked in its own test task.
+ const doc = card.ownerDocument;
+ if (card.addon.canChangeQuarantineIgnored) {
+ let row = rows.shift();
+ await checkLabel(row, "quarantined-domains");
+ await checkRowScreenReaderAccessibility(row, {
+ groupName: "quarantined domains exempt controls",
+ expectedFluentId: "addon-detail-group-label-quarantined-domains",
+ });
+
+ // quarantineIgnoredByUser UI help text.
+ row = rows.shift();
+ ok(row.classList.contains("addon-detail-help-row"), "There's a help row");
+ ok(!row.hidden, "The help row is shown");
+ is(
+ doc.l10n.getAttributes(row.firstElementChild).id,
+ "addon-detail-quarantined-domains-help",
+ "The help row is for quarantined domains"
+ );
+ }
+}
+
+function formatUrl(contentAttribute, url) {
+ let parsedUrl = new URL(url);
+ parsedUrl.searchParams.set("utm_source", "firefox-browser");
+ parsedUrl.searchParams.set("utm_medium", "firefox-browser");
+ parsedUrl.searchParams.set("utm_content", contentAttribute);
+ return parsedUrl.href;
+}
+
+function checkLink(link, url, text = url) {
+ ok(link, "There is a link");
+ is(link.href, url, "The link goes to the URL");
+ if (text instanceof Object) {
+ // Check the fluent data.
+ Assert.deepEqual(
+ link.ownerDocument.l10n.getAttributes(link),
+ text,
+ "The fluent data is set correctly"
+ );
+ } else {
+ // Just check text.
+ is(link.textContent, text, "The text is set");
+ }
+ is(link.getAttribute("target"), "_blank", "The link opens in a new tab");
+}
+
+function checkOptions(doc, options, expectedOptions) {
+ let numOptions = expectedOptions.length;
+ is(options.length, numOptions, `There are ${numOptions} options`);
+ for (let i = 0; i < numOptions; i++) {
+ let option = options[i];
+ is(option.children.length, 2, "There are 2 children for the option");
+ let input = option.firstElementChild;
+ is(input.tagName, "INPUT", "The input is first");
+ let text = option.lastElementChild;
+ is(text.tagName, "SPAN", "The label text is second");
+ let expected = expectedOptions[i];
+ is(input.value, expected.value, "The value is right");
+ is(input.checked, expected.checked, "The checked property is correct");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(text),
+ { id: expected.label, args: null },
+ "The label has the right text"
+ );
+ }
+}
+
+function assertDeckHeadingHidden(group) {
+ ok(group.hidden, "The tab group is hidden");
+ let buttons = group.querySelectorAll(".tab-button");
+ for (let button of buttons) {
+ Assert.equal(button.offsetHeight, 0, `The ${button.name} is hidden`);
+ }
+}
+
+function assertDeckHeadingButtons(group, visibleButtons) {
+ ok(!group.hidden, "The tab group is shown");
+ let buttons = group.querySelectorAll(".tab-button");
+ Assert.greaterOrEqual(
+ buttons.length,
+ visibleButtons.length,
+ `There should be at least ${visibleButtons.length} buttons`
+ );
+ for (let button of buttons) {
+ if (visibleButtons.includes(button.name)) {
+ ok(!button.hidden, `The ${button.name} is shown`);
+ } else {
+ ok(button.hidden, `The ${button.name} is hidden`);
+ }
+ }
+}
+
+async function hasPrivateAllowed(id) {
+ let perms = await ExtensionPermissions.get(id);
+ return perms.permissions.includes("internal:privateBrowsingAllowed");
+}
+
+async function assertBackButtonIsDisabled(win) {
+ let backButton = await BrowserTestUtils.waitForCondition(async () => {
+ let backButton = win.document.querySelector(".back-button");
+
+ // Wait until the button is visible in the page.
+ return backButton && !backButton.hidden ? backButton : false;
+ });
+
+ ok(backButton, "back button is rendered");
+ ok(backButton.disabled, "back button is disabled");
+}
+
+add_setup(async function enableHtmlViews() {
+ gProvider = new MockProvider(["extension", "sitepermission"]);
+ gProvider.createAddons([
+ {
+ id: "addon1@mochi.test",
+ name: "Test add-on 1",
+ creator: { name: "The creator", url: "http://addons.mozilla.org/me" },
+ version: "3.1",
+ description: "Short description",
+ fullDescription: "Longer description\nWith brs!",
+ type: "extension",
+ contributionURL: "http://example.com/contribute",
+ averageRating: 4.279,
+ userPermissions: {
+ origins: ["<all_urls>", "file://*/*"],
+ permissions: ["alarms", "contextMenus", "tabs", "webNavigation"],
+ },
+ reviewCount: 5,
+ reviewURL: "http://addons.mozilla.org/reviews",
+ homepageURL: "http://example.com/addon1",
+ updateDate: new Date("2019-03-07T01:00:00"),
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE,
+ },
+ {
+ id: "addon2@mochi.test",
+ name: "Test add-on 2",
+ creator: { name: "I made it" },
+ description: "Short description",
+ userPermissions: {
+ origins: [],
+ permissions: ["alarms", "contextMenus"],
+ },
+ type: "extension",
+ },
+ {
+ id: "addon3@mochi.test",
+ name: "Test add-on 3",
+ creator: { name: "Look a super long description" },
+ description: "Short description",
+ fullDescription: "Mozilla\n".repeat(100),
+ userPermissions: {
+ origins: [],
+ permissions: ["alarms", "contextMenus"],
+ },
+ type: "extension",
+ contributionURL: "http://example.com/contribute",
+ updateDate: new Date("2022-03-07T01:00:00"),
+ },
+ {
+ id: "addon4@mochi.test",
+ name: "Test add-on 4",
+ creator: { name: "Some name" },
+ description: "Short description",
+ userPermissions: {
+ origins: [],
+ permissions: ["alarms", "contextMenus"],
+ },
+ type: "extension",
+ reviewCount: 0,
+ reviewURL: "http://addons.mozilla.org/reviews",
+ averageRating: 0,
+ },
+ {
+ // NOTE: Keep the mock properties in sync with the one that
+ // SitePermsAddonWrapper would be providing in real synthetic
+ // addon entries managed by the SitePermsAddonProvider.
+ id: "sitepermission@mochi.test",
+ version: "2.0",
+ name: "Test site permission add-on",
+ description: "permission description",
+ fullDescription: "detailed description",
+ siteOrigin: "http://mochi.test",
+ sitePermissions: ["midi"],
+ type: "sitepermission",
+ permissions: AddonManager.PERM_CAN_UNINSTALL,
+ },
+ {
+ id: "theme1@mochi.test",
+ name: "Test theme",
+ creator: { name: "Artist", url: "http://example.com/artist" },
+ description: "A nice tree",
+ type: "theme",
+ screenshots: [
+ {
+ url: "http://example.com/preview-wide.png",
+ width: 760,
+ height: 92,
+ },
+ {
+ url: "http://example.com/preview.png",
+ width: 680,
+ height: 92,
+ },
+ ],
+ },
+ ]);
+
+ promptService = mockPromptService();
+});
+
+add_task(async function testOpenDetailView() {
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ let id2 = "test2@mochi.test";
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test",
+ browser_specific_settings: { gecko: { id: id2 } },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension2.startup();
+
+ const goBack = async win => {
+ let loaded = waitForViewLoad(win);
+ let backButton = win.document.querySelector(".back-button");
+ ok(!backButton.disabled, "back button is enabled");
+ backButton.click();
+ await loaded;
+ };
+
+ let win = await loadInitialView("extension");
+
+ // Test click on card to open details.
+ let card = getAddonCard(win, id);
+ ok(!card.querySelector("addon-details"), "The card doesn't have details");
+ let loaded = waitForViewLoad(win);
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive container to open the card
+ // with a mouse, while its inner link element is accessible and is being
+ // tested in other test cases, thus this rule check shall be ignored by
+ // a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win);
+ AccessibilityUtils.resetEnv();
+ await loaded;
+
+ card = getAddonCard(win, id);
+ ok(card.querySelector("addon-details"), "The card now has details");
+
+ await goBack(win);
+
+ // Test using more options menu.
+ card = getAddonCard(win, id);
+ loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(win, id);
+ ok(card.querySelector("addon-details"), "The card now has details");
+
+ await goBack(win);
+
+ card = getAddonCard(win, id2);
+ loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ await goBack(win);
+
+ // Test click on add-on name.
+ card = getAddonCard(win, id2);
+ ok(!card.querySelector("addon-details"), "The card isn't expanded");
+ let addonName = card.querySelector(".addon-name");
+ loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(addonName, {}, win);
+ await loaded;
+ card = getAddonCard(win, id2);
+ ok(card.querySelector("addon-details"), "The card is expanded");
+
+ await closeView(win);
+ await extension.unload();
+ await extension2.unload();
+});
+
+add_task(async function testDetailOperations() {
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(win, id);
+ ok(!card.querySelector("addon-details"), "The card doesn't have details");
+ let loaded = waitForViewLoad(win);
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive container to open the card
+ // with a mouse, while its inner link element is accessible and is being
+ // tested in other test cases, thus this rule check shall be ignored by
+ // a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win);
+ AccessibilityUtils.resetEnv();
+ await loaded;
+
+ card = getAddonCard(win, id);
+ let panel = card.querySelector("panel-list");
+
+ // Check button visibility.
+ let disableButton = card.querySelector('[action="toggle-disabled"]');
+ ok(!disableButton.hidden, "The disable button is visible");
+
+ let removeButton = panel.querySelector('[action="remove"]');
+ ok(!removeButton.hidden, "The remove button is visible");
+
+ let separator = panel.querySelector("hr:last-of-type");
+ ok(separator.hidden, "The separator is hidden");
+
+ let expandButton = panel.querySelector('[action="expand"]');
+ ok(expandButton.hidden, "The expand button is hidden");
+
+ // Check toggling disabled.
+ let name = card.addonNameEl;
+ is(name.textContent, "Test", "The name is set when enabled");
+ is(doc.l10n.getAttributes(name).id, null, "There is no l10n name");
+
+ // Disable the extension.
+ let disableToggled = BrowserTestUtils.waitForEvent(card, "update");
+ disableButton.click();
+ await disableToggled;
+
+ // The (disabled) text should be shown now.
+ Assert.deepEqual(
+ doc.l10n.getAttributes(name),
+ { id: "addon-name-disabled", args: { name: "Test" } },
+ "The name is updated to the disabled text"
+ );
+
+ // Enable the add-on.
+ let extensionStarted = AddonTestUtils.promiseWebExtensionStartup(id);
+ disableToggled = BrowserTestUtils.waitForEvent(card, "update");
+ disableButton.click();
+ await Promise.all([disableToggled, extensionStarted]);
+
+ // Name is just the add-on name again.
+ is(name.textContent, "Test", "The name is reset when enabled");
+ is(doc.l10n.getAttributes(name).id, null, "There is no l10n name");
+
+ // Remove but cancel.
+ let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled");
+ removeButton.click();
+ await cancelled;
+
+ // Remove the extension.
+ let viewChanged = waitForViewLoad(win);
+ // Tell the mock prompt service that the prompt was accepted.
+ promptService._response = 0;
+ removeButton.click();
+ await viewChanged;
+
+ // We're on the list view now and there's no card for this extension.
+ const addonList = doc.querySelector("addon-list");
+ ok(addonList, "There's an addon-list now");
+ ok(!getAddonCard(win, id), "The extension no longer has a card");
+ let addon = await AddonManager.getAddonByID(id);
+ ok(
+ addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "The addon is pending uninstall"
+ );
+
+ // Ensure that a pending uninstall bar has been created for the
+ // pending uninstall extension, and pressing the undo button will
+ // refresh the list and render a card to the re-enabled extension.
+ assertHasPendingUninstalls(addonList, 1);
+ assertHasPendingUninstallAddon(addonList, addon);
+
+ extensionStarted = AddonTestUtils.promiseWebExtensionStartup(addon.id);
+ await testUndoPendingUninstall(addonList, addon);
+ info("Wait for the pending uninstall addon complete restart");
+ await extensionStarted;
+
+ card = getAddonCard(win, addon.id);
+ ok(card, "Addon card rendered after clicking pending uninstall undo button");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testFullDetails() {
+ let id = "addon1@mochi.test";
+ let headingId = "addon1_mochi_test-heading";
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // The list card.
+ let card = getAddonCard(win, id);
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+
+ // Make sure the preview is hidden.
+ let preview = card.querySelector(".card-heading-image");
+ ok(preview, "There is a preview");
+ is(preview.hidden, true, "The preview is hidden");
+
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // This is now the detail card.
+ card = getAddonCard(win, id);
+ ok(card.hasAttribute("expanded"), "The detail card is expanded");
+
+ let cardHeading = card.querySelector("h1");
+ is(cardHeading.textContent, "Test add-on 1", "Card heading is set");
+ is(cardHeading.id, headingId, "Heading has correct id");
+ is(
+ card.querySelector(".card").getAttribute("aria-labelledby"),
+ headingId,
+ "Card is labelled by the heading"
+ );
+
+ // Make sure the preview is hidden.
+ preview = card.querySelector(".card-heading-image");
+ ok(preview, "There is a preview");
+ is(preview.hidden, true, "The preview is hidden");
+
+ let details = card.querySelector("addon-details");
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]);
+
+ let desc = details.querySelector(".addon-detail-description");
+ is(
+ desc.innerHTML,
+ "Longer description<br>With brs!",
+ "The full description replaces newlines with <br>"
+ );
+
+ let sitepermissionsRow = details.querySelector(
+ ".addon-detail-sitepermissions"
+ );
+ is(
+ sitepermissionsRow.hidden,
+ true,
+ "AddonSitePermissionsList should be hidden for this addon type"
+ );
+
+ // Check the show more button is not there
+ const showMoreBtn = card.querySelector(".addon-detail-description-toggle");
+ ok(showMoreBtn.hidden, "The show more button is not visible");
+
+ let contrib = details.querySelector(".addon-detail-contribute");
+ ok(contrib, "The contribution section is visible");
+
+ let waitForTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://example.com/contribute"
+ );
+ contrib.querySelector("button").click();
+ BrowserTestUtils.removeTab(await waitForTab);
+
+ let rows = getDetailRows(card);
+
+ // Auto updates.
+ let row = rows.shift();
+
+ await checkLabel(row, "updates");
+ await checkRowScreenReaderAccessibility(row, {
+ groupName: "updates controls",
+ expectedFluentId: "addon-detail-group-label-updates",
+ });
+
+ let expectedOptions = [
+ { value: "1", label: "addon-detail-updates-radio-default", checked: false },
+ { value: "2", label: "addon-detail-updates-radio-on", checked: true },
+ { value: "0", label: "addon-detail-updates-radio-off", checked: false },
+ ];
+ let options = row.lastElementChild.querySelectorAll("label");
+ checkOptions(doc, options, expectedOptions);
+
+ // Private browsing, functionality checked in another test.
+ row = rows.shift();
+ await checkLabel(row, "private-browsing");
+ await checkRowScreenReaderAccessibility(row, {
+ groupName: "private browsing controls",
+ expectedFluentId: "addon-detail-group-label-private-browsing",
+ });
+
+ // Private browsing help text.
+ row = rows.shift();
+ ok(row.classList.contains("addon-detail-help-row"), "There's a help row");
+ ok(!row.hidden, "The help row is shown");
+ is(
+ doc.l10n.getAttributes(row).id,
+ "addon-detail-private-browsing-help",
+ "The help row is for private browsing"
+ );
+
+ await checkQuarantinedDomainsUserAllowedRows(card, rows);
+
+ // Author.
+ row = rows.shift();
+ await checkLabel(row, "author");
+ let link = row.querySelector("a");
+ let authorLink = formatUrl(
+ "addons-manager-user-profile-link",
+ "http://addons.mozilla.org/me"
+ );
+ checkLink(link, authorLink, "The creator");
+
+ // Version.
+ row = rows.shift();
+ await checkLabel(row, "version");
+ let text = row.lastChild;
+ is(text.textContent, "3.1", "The version is set");
+
+ // Last updated.
+ row = rows.shift();
+ await checkLabel(row, "last-updated");
+ text = row.lastChild;
+ is(text.textContent, "March 7, 2019", "The last updated date is set");
+
+ // Homepage.
+ row = rows.shift();
+ await checkLabel(row, "homepage");
+ link = row.querySelector("a");
+ checkLink(link, "http://example.com/addon1");
+
+ // Reviews.
+ row = rows.shift();
+ await checkLabel(row, "rating");
+ let rating = row.lastElementChild;
+ ok(rating.classList.contains("addon-detail-rating"), "Found the rating el");
+ let mozFiveStar = rating.querySelector("moz-five-star");
+ is(mozFiveStar.rating, 4.279, "Exact rating used for calculations");
+ let stars = Array.from(mozFiveStar.starEls);
+ let fullAttrs = stars.map(star => star.getAttribute("fill")).join(",");
+ is(fullAttrs, "full,full,full,full,half", "Four and a half stars are full");
+ link = rating.querySelector("a");
+ let reviewsLink = formatUrl(
+ "addons-manager-reviews-link",
+ "http://addons.mozilla.org/reviews"
+ );
+ checkLink(link, reviewsLink, {
+ id: "addon-detail-reviews-link",
+ args: { numberOfReviews: 5 },
+ });
+
+ // While we are here, let's test edge cases of star ratings.
+ async function testRating(rating, ratingRounded, expectation) {
+ mozFiveStar.rating = rating;
+ await mozFiveStar.updateComplete;
+ if (mozFiveStar.ownerDocument.hasPendingL10nMutations) {
+ await BrowserTestUtils.waitForEvent(
+ mozFiveStar.ownerDocument,
+ "L10nMutationsFinished"
+ );
+ }
+ let starsString = Array.from(mozFiveStar.starEls)
+ .map(star => star.getAttribute("fill"))
+ .join(",");
+ is(starsString, expectation, `Rendering of rating ${rating}`);
+
+ is(
+ mozFiveStar.starsWrapperEl.title,
+ `Rated ${ratingRounded} out of 5`,
+ "Rendered title must contain at most one fractional digit"
+ );
+ }
+ await testRating(0.0, "0", "empty,empty,empty,empty,empty");
+ await testRating(0.123, "0.1", "empty,empty,empty,empty,empty");
+ await testRating(0.249, "0.2", "empty,empty,empty,empty,empty");
+ await testRating(0.25, "0.3", "half,empty,empty,empty,empty");
+ await testRating(0.749, "0.7", "half,empty,empty,empty,empty");
+ await testRating(0.75, "0.8", "full,empty,empty,empty,empty");
+ await testRating(1.0, "1", "full,empty,empty,empty,empty");
+ await testRating(4.249, "4.2", "full,full,full,full,empty");
+ await testRating(4.25, "4.3", "full,full,full,full,half");
+ await testRating(4.749, "4.7", "full,full,full,full,half");
+ await testRating(5.0, "5", "full,full,full,full,full");
+
+ // That should've been all the rows.
+ is(rows.length, 0, "There are no more rows left");
+
+ await closeView(win);
+});
+
+add_task(async function testFullDetailsShowMoreButton() {
+ const id = "addon3@mochi.test";
+ const win = await loadInitialView("extension");
+
+ // The list card.
+ let card = getAddonCard(win, id);
+ const loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // This is now the detail card.
+ card = getAddonCard(win, id);
+
+ // Check the show more button is there
+ const showMoreBtn = card.querySelector(".addon-detail-description-toggle");
+ ok(!showMoreBtn.hidden, "The show more button is visible");
+
+ const descriptionWrapper = card.querySelector(
+ ".addon-detail-description-wrapper"
+ );
+ ok(
+ descriptionWrapper.classList.contains("addon-detail-description-collapse"),
+ "The long description is collapsed"
+ );
+
+ // After click the description should be expanded
+ showMoreBtn.click();
+ ok(
+ !descriptionWrapper.classList.contains("addon-detail-description-collapse"),
+ "The long description is expanded"
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testMinimalExtension() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(win, "addon2@mochi.test");
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(win, "addon2@mochi.test");
+ let details = card.querySelector("addon-details");
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]);
+
+ let desc = details.querySelector(".addon-detail-description");
+ is(desc.textContent, "", "There is no full description");
+
+ let contrib = details.querySelector(".addon-detail-contribute");
+ ok(contrib.hidden, "The contribution element is hidden");
+
+ let rows = getDetailRows(card);
+
+ // Automatic updates.
+ let row = rows.shift();
+ await checkLabel(row, "updates");
+
+ // Private browsing settings.
+ row = rows.shift();
+ await checkLabel(row, "private-browsing");
+
+ // Private browsing help text.
+ row = rows.shift();
+ ok(row.classList.contains("addon-detail-help-row"), "There's a help row");
+ ok(!row.hidden, "The help row is shown");
+ is(
+ doc.l10n.getAttributes(row).id,
+ "addon-detail-private-browsing-help",
+ "The help row is for private browsing"
+ );
+
+ await checkQuarantinedDomainsUserAllowedRows(card, rows);
+
+ // Author.
+ row = rows.shift();
+ await checkLabel(row, "author");
+ let text = row.lastChild;
+ is(text.textContent, "I made it", "The author is set");
+ ok(Text.isInstance(text), "The author is a text node");
+
+ is(rows.length, 0, "There are no more rows");
+
+ await closeView(win);
+});
+
+add_task(async function testDefaultTheme() {
+ let win = await loadInitialView("theme");
+
+ // The list card.
+ let card = getAddonCard(win, DEFAULT_THEME_ID);
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+
+ let preview = card.querySelector(".card-heading-image");
+ ok(preview, "There is a preview");
+ ok(!preview.hidden, "The preview is visible");
+
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(win, DEFAULT_THEME_ID);
+
+ preview = card.querySelector(".card-heading-image");
+ ok(preview, "There is a preview");
+ ok(!preview.hidden, "The preview is visible");
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingHidden(card.details.tabGroup);
+
+ let rows = getDetailRows(card);
+
+ // Author.
+ let author = rows.shift();
+ await checkLabel(author, "author");
+ let text = author.lastChild;
+ is(text.textContent, "Mozilla", "The author is set");
+
+ // Version.
+ let version = rows.shift();
+ await checkLabel(version, "version");
+ is(version.lastChild.textContent, "1.3", "It's always version 1.3");
+
+ // Last updated.
+ let lastUpdated = rows.shift();
+ await checkLabel(lastUpdated, "last-updated");
+ let dateText = lastUpdated.lastChild.textContent;
+ ok(dateText, "There is a date set");
+ ok(!dateText.includes("Invalid Date"), `"${dateText}" should be a date`);
+
+ is(rows.length, 0, "There are no more rows");
+
+ await closeView(win);
+});
+
+add_task(async function testStaticTheme() {
+ let win = await loadInitialView("theme");
+
+ // The list card.
+ let card = getAddonCard(win, "theme1@mochi.test");
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+
+ // Make sure the preview is set.
+ let preview = card.querySelector(".card-heading-image");
+ ok(preview, "There is a preview");
+ is(preview.src, "http://example.com/preview.png", "The preview URL is set");
+ is(preview.width, 664, "The width is set");
+ is(preview.height, 90, "The height is set");
+ is(preview.hidden, false, "The preview is visible");
+
+ // Load the detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(win, "theme1@mochi.test");
+
+ // Make sure the preview is still set.
+ preview = card.querySelector(".card-heading-image");
+ ok(preview, "There is a preview");
+ is(preview.src, "http://example.com/preview.png", "The preview URL is set");
+ is(preview.width, 664, "The width is set");
+ is(preview.height, 90, "The height is set");
+ is(preview.hidden, false, "The preview is visible");
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingHidden(card.details.tabGroup);
+
+ let rows = getDetailRows(card);
+
+ // Automatic updates.
+ let row = rows.shift();
+ await checkLabel(row, "updates");
+
+ // Author.
+ let author = rows.shift();
+ await checkLabel(author, "author");
+ let text = author.lastElementChild;
+ is(text.textContent, "Artist", "The author is set");
+
+ is(rows.length, 0, "There was only 1 row");
+
+ await closeView(win);
+});
+
+add_task(async function testSitePermission() {
+ let win = await loadInitialView("sitepermission");
+
+ // The list card.
+ let card = getAddonCard(win, "sitepermission@mochi.test");
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+
+ // Load the detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(win, "sitepermission@mochi.test");
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingHidden(card.details.tabGroup);
+
+ let sitepermissionsRow = card.querySelector(".addon-detail-sitepermissions");
+ is(
+ BrowserTestUtils.isVisible(sitepermissionsRow),
+ true,
+ "AddonSitePermissionsList should be visible for this addon type"
+ );
+
+ let [versionRow, ...restRows] = getDetailRows(card);
+ await checkLabel(versionRow, "version");
+
+ Assert.deepEqual(
+ restRows.map(row => row.getAttribute("class")),
+ [],
+ "All other details row are hidden as expected"
+ );
+
+ let permissions = Array.from(
+ card.querySelectorAll(".addon-permissions-list .permission-info")
+ );
+ is(permissions.length, 1, "a permission is listed");
+ is(permissions[0].textContent, "Access MIDI devices", "got midi permission");
+
+ await closeView(win);
+});
+
+add_task(async function testPrivateBrowsingExtension() {
+ let id = "pb@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "My PB extension",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // The add-on shouldn't show that it's allowed yet.
+ let card = getAddonCard(win, id);
+ let badge = card.querySelector(".addon-badge-private-browsing-allowed");
+ ok(badge.hidden, "The PB badge is hidden initially");
+ ok(!(await hasPrivateAllowed(id)), "PB is not allowed");
+
+ // Load the detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // The badge is still hidden on the detail view.
+ card = getAddonCard(win, id);
+ badge = card.querySelector(".addon-badge-private-browsing-allowed");
+ ok(badge.hidden, "The PB badge is hidden on the detail view");
+ ok(!(await hasPrivateAllowed(id)), "PB is not allowed");
+
+ let pbRow = card.querySelector(".addon-detail-row-private-browsing");
+ let name = card.querySelector(".addon-name");
+
+ // Allow private browsing.
+ let [allow, disallow] = pbRow.querySelectorAll("input");
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+
+ // Check that the disabled state isn't shown while reloading the add-on.
+ let addonDisabled = AddonTestUtils.promiseAddonEvent("onDisabled");
+ allow.click();
+ await addonDisabled;
+ is(
+ doc.l10n.getAttributes(name).id,
+ null,
+ "The disabled message is not shown for the add-on"
+ );
+
+ // Check the PB stuff.
+ await updated;
+
+ // Not sure what better to await here.
+ await TestUtils.waitForCondition(() => !badge.hidden);
+
+ ok(!badge.hidden, "The PB badge is now shown");
+ ok(await hasPrivateAllowed(id), "PB is allowed");
+ is(
+ doc.l10n.getAttributes(name).id,
+ null,
+ "The disabled message is not shown for the add-on"
+ );
+
+ info("Verify the badge links to the support page");
+ let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, PB_SUMO_URL);
+ EventUtils.synthesizeMouseAtCenter(badge, {}, win);
+ let tab = await tabOpened;
+ BrowserTestUtils.removeTab(tab);
+
+ // Disable the add-on and change the value.
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ card.querySelector('[action="toggle-disabled"]').click();
+ await updated;
+
+ // It's still allowed in PB.
+ ok(await hasPrivateAllowed(id), "PB is allowed");
+ ok(!badge.hidden, "The PB badge is shown");
+
+ // Disallow PB.
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ disallow.click();
+ await updated;
+
+ ok(badge.hidden, "The PB badge is hidden");
+ ok(!(await hasPrivateAllowed(id)), "PB is disallowed");
+
+ // Allow PB.
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ allow.click();
+ await updated;
+
+ ok(!badge.hidden, "The PB badge is hidden");
+ ok(await hasPrivateAllowed(id), "PB is disallowed");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testInvalidExtension() {
+ let win = await open_manager("addons://detail/foo");
+ let categoryUtils = new CategoryUtilities(win);
+ is(
+ categoryUtils.selectedCategory,
+ "discover",
+ "Should fall back to the discovery pane"
+ );
+
+ ok(!gBrowser.canGoBack, "The view has been replaced");
+
+ await close_manager(win);
+});
+
+add_task(async function testInvalidExtensionNoDiscover() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.getAddons.showPane", false]],
+ });
+
+ let win = await open_manager("addons://detail/foo");
+ let categoryUtils = new CategoryUtilities(win);
+ is(
+ categoryUtils.selectedCategory,
+ "extension",
+ "Should fall back to the extension list if discover is disabled"
+ );
+
+ ok(!gBrowser.canGoBack, "The view has been replaced");
+
+ await close_manager(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testExternalUninstall() {
+ let id = "remove@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Remove me",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let addon = await AddonManager.getAddonByID(id);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Load the detail view.
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ let detailsLoaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await detailsLoaded;
+
+ // Uninstall the add-on with undo. Should go to extension list.
+ let listLoaded = waitForViewLoad(win);
+ await addon.uninstall(true);
+ await listLoaded;
+
+ // Verify the list view was loaded and the card is gone.
+ let list = doc.querySelector("addon-list");
+ ok(list, "Moved to a list page");
+ is(list.type, "extension", "We're on the extension list page");
+ card = list.querySelector(`addon-card[addon-id="${id}"]`);
+ ok(!card, "The card has been removed");
+
+ await extension.unload();
+ closeView(win);
+});
+
+add_task(async function testExternalThemeUninstall() {
+ let id = "remove-theme@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ name: "Remove theme",
+ theme: {},
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let addon = await AddonManager.getAddonByID(id);
+
+ let win = await loadInitialView("theme");
+ let doc = win.document;
+
+ // Load the detail view.
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ let detailsLoaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await detailsLoaded;
+
+ // Uninstall the add-on without undo. Should go to theme list.
+ let listLoaded = waitForViewLoad(win);
+ await addon.uninstall();
+ await listLoaded;
+
+ // Verify the list view was loaded and the card is gone.
+ let list = doc.querySelector("addon-list");
+ ok(list, "Moved to a list page");
+ is(list.type, "theme", "We're on the theme list page");
+ card = list.querySelector(`addon-card[addon-id="${id}"]`);
+ ok(!card, "The card has been removed");
+
+ await extension.unload();
+ closeView(win);
+});
+
+add_task(async function testPrivateBrowsingAllowedListView() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Allowed PB extension",
+ browser_specific_settings: { gecko: { id: "allowed@mochi.test" } },
+ },
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+ let perms = { permissions: ["internal:privateBrowsingAllowed"], origins: [] };
+ await ExtensionPermissions.add("allowed@mochi.test", perms);
+ let addon = await AddonManager.getAddonByID("allowed@mochi.test");
+ await addon.reload();
+
+ let win = await loadInitialView("extension");
+
+ // The allowed extension should have a badge on load.
+ let card = getAddonCard(win, "allowed@mochi.test");
+ let badge = card.querySelector(".addon-badge-private-browsing-allowed");
+ ok(!badge.hidden, "The PB badge is shown for the allowed add-on");
+
+ await extension.unload();
+ await closeView(win);
+});
+
+// When the back button is used, its disabled state will be updated. If it
+// isn't updated when showing a view, then it will be disabled on the next
+// use (bug 1551213) if the last use caused it to become disabled.
+add_task(async function testGoBackButton() {
+ // Make sure the list view is the first loaded view so you cannot go back.
+ Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/extension");
+
+ let id = "addon1@mochi.test";
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let backButton = doc.querySelector(".back-button");
+
+ let loadDetailView = () => {
+ let loaded = waitForViewLoad(win);
+ getAddonCard(win, id).querySelector("[action=expand]").click();
+ return loaded;
+ };
+
+ let checkBackButtonState = () => {
+ is_element_visible(backButton, "Back button is visible on the detail page");
+ ok(!backButton.disabled, "Back button is enabled");
+ };
+
+ // Load the detail view, first time should be fine.
+ await loadDetailView();
+ checkBackButtonState();
+
+ // Use the back button directly to pop from history and trigger its disabled
+ // state to be updated.
+ let loaded = waitForViewLoad(win);
+ backButton.click();
+ await loaded;
+
+ await loadDetailView();
+ checkBackButtonState();
+
+ await closeView(win);
+});
+
+add_task(async function testEmptyMoreOptionsMenu() {
+ let theme = await AddonManager.getAddonByID(DEFAULT_THEME_ID);
+ ok(theme.isActive, "The default theme is enabled");
+
+ let win = await loadInitialView("theme");
+
+ let card = getAddonCard(win, DEFAULT_THEME_ID);
+ let enabledItems = card.options.visibleItems;
+ is(enabledItems.length, 1, "There is one enabled item");
+ is(enabledItems[0].getAttribute("action"), "expand", "Expand is enabled");
+ let moreOptionsButton = card.querySelector(".more-options-button");
+ ok(!moreOptionsButton.hidden, "The more options button is visible");
+
+ let loaded = waitForViewLoad(win);
+ enabledItems[0].click();
+ await loaded;
+
+ card = getAddonCard(win, DEFAULT_THEME_ID);
+ let toggleDisabledButton = card.querySelector('[action="toggle-disabled"]');
+ enabledItems = card.options.visibleItems;
+ is(enabledItems.length, 0, "There are no enabled items");
+ moreOptionsButton = card.querySelector(".more-options-button");
+ ok(moreOptionsButton.hidden, "The more options button is now hidden");
+ ok(toggleDisabledButton.hidden, "The disable button is hidden");
+
+ // Switch themes, the menu should be hidden, but enable button should appear.
+ let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID);
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ await darkTheme.enable();
+ await updated;
+
+ ok(moreOptionsButton.hidden, "The more options button is still hidden");
+ ok(!toggleDisabledButton.hidden, "The enable button is visible");
+
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ await toggleDisabledButton.click();
+ await updated;
+
+ ok(moreOptionsButton.hidden, "The more options button is hidden");
+ ok(toggleDisabledButton.hidden, "The disable button is hidden");
+
+ await closeView(win);
+});
+
+add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmpty() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { name: "Test Go Back Button" },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let viewID = `addons://detail/${encodeURIComponent(extension.id)}`;
+
+ // When we have a fresh new tab, `about:addons` is opened in it.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null);
+ // Simulate a click on "Manage extension" from a context menu.
+ let win = await BrowserOpenAddonsMgr(viewID);
+ await assertBackButtonIsDisabled(win);
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmptyInNewTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { name: "Test Go Back Button" },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let viewID = `addons://detail/${encodeURIComponent(extension.id)}`;
+
+ // When we have a tab with a page loaded, `about:addons` will be opened in a
+ // new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org"
+ );
+ let addonsTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+ // Simulate a click on "Manage extension" from a context menu.
+ let win = await BrowserOpenAddonsMgr(viewID);
+ let addonsTab = await addonsTabLoaded;
+ await assertBackButtonIsDisabled(win);
+
+ BrowserTestUtils.removeTab(addonsTab);
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function testGoBackButtonIsDisabledAfterBrowserBackButton() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { name: "Test Go Back Button" },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let viewID = `addons://detail/${encodeURIComponent(extension.id)}`;
+
+ // When we have a fresh new tab, `about:addons` is opened in it.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null);
+ // Simulate a click on "Manage extension" from a context menu.
+ let win = await BrowserOpenAddonsMgr(viewID);
+ await assertBackButtonIsDisabled(win);
+
+ // Navigate to the extensions list.
+ await new CategoryUtilities(win).openType("extension");
+
+ // Click on the browser back button.
+ gBrowser.goBack();
+ await assertBackButtonIsDisabled(win);
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function testQuarantinedDomainsUserAllowedUI() {
+ let regularExtId = "regular@mochi.test";
+ let privilegedExtId = "privileged@mochi.test";
+ let recommendedExtId = "recommended@mochi.test";
+ let themeId = "theme@mochi.test";
+ let provider = new MockProvider();
+ provider.createAddons([
+ {
+ id: privilegedExtId,
+ isPrivileged: true,
+ name: "A privileged extension",
+ type: "extension",
+ quarantineIgnoredByApp: true,
+ quarantineIgnoredByUser: false,
+ canChangeQuarantineIgnored: false,
+ },
+ {
+ id: recommendedExtId,
+ isRecommended: true,
+ recommendationStates: ["recommended"],
+ name: "A Recommended extension",
+ type: "extension",
+ quarantineIgnoredByApp: true,
+ quarantineIgnoredByUser: false,
+ canChangeQuarantineIgnored: false,
+ },
+ {
+ id: themeId,
+ name: "A fake regular theme",
+ type: "theme",
+ canChangeQuarantineIgnored: false,
+ },
+ ]);
+
+ let regularExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Some regular extension",
+ browser_specific_settings: { gecko: { id: regularExtId } },
+ },
+ useAddonManager: "permanent",
+ });
+
+ async function testQuarantinedUserAllowedUIRows(id, { expectVisible }) {
+ const perAddonPref = QuarantinedDomains.getUserAllowedAddonIdPrefName(id);
+ Services.prefs.clearUserPref(perAddonPref);
+
+ let card = getAddonCard(win, id);
+
+ const cardDetails = card.querySelector("addon-details");
+ ok(cardDetails, "Card details found");
+ const quarantinedUserAllowedControlsRow = cardDetails.querySelector(
+ ".addon-detail-row-quarantined-domains"
+ );
+
+ ok(
+ quarantinedUserAllowedControlsRow,
+ "Found quarantine domains controls row element"
+ );
+
+ is(
+ BrowserTestUtils.isVisible(quarantinedUserAllowedControlsRow),
+ expectVisible,
+ `Expect quarantineIgnoreByUser UI to ${
+ expectVisible ? "be" : "NOT be"
+ } visible`
+ );
+ const helpRow = quarantinedUserAllowedControlsRow.nextElementSibling;
+ is(
+ helpRow.classList.contains("addon-detail-help-row"),
+ true,
+ "Expect next sibling to be an addon-detail-help-row"
+ );
+ is(
+ BrowserTestUtils.isVisible(helpRow),
+ expectVisible,
+ `Expect quarantineIgnoredByUser UI help to ${
+ expectVisible ? "be" : "NOT be"
+ } visible`
+ );
+
+ if (!expectVisible) {
+ // The assertion that follows are going to be executed when the
+ // test helper function is called for an addon card detail view
+ // for which the quarantined domains rows are expected to be
+ // visible.
+ return;
+ }
+
+ is(
+ doc.l10n.getAttributes(helpRow.firstElementChild).id,
+ "addon-detail-quarantined-domains-help",
+ "Expect addon-detail-help-row to be localized"
+ );
+ const helpSupportLink = helpRow.querySelector("[is=moz-support-link]");
+ ok(helpSupportLink, "Expect a moz-support-link");
+ is(
+ helpSupportLink?.getAttribute("support-page"),
+ "quarantined-domains",
+ "Expect support link to point to SUMO quarantined-domains page"
+ );
+ // Make sure none of the elements in the help row are missing
+ // the expected strings associated to the fluent ids being set
+ // (if any is missing, l10n.translateElements will reject and
+ // trigger an explicit test failure);
+ await doc.l10n.translateElements([helpRow]);
+
+ const radioInputs = Array.from(
+ quarantinedUserAllowedControlsRow.querySelectorAll(
+ "input[name=quarantined-domains-user-allowed]"
+ )
+ );
+
+ Assert.deepEqual(
+ radioInputs.map(el => el.value),
+ ["1", "0"],
+ "Got the expected radio inputs values"
+ );
+
+ Assert.deepEqual(
+ radioInputs.map(el => doc.l10n.getAttributes(el.nextElementSibling).id),
+ ["allow", "disallow"].map(
+ txt => `addon-detail-quarantined-domains-${txt}`
+ ),
+ "Got the expected fluent ids on the radio input text"
+ );
+
+ const checkRadioInputsState = ({ expectUserAllowed }) => {
+ is(
+ card.addon.quarantineIgnoredByUser,
+ expectUserAllowed,
+ `Expect the test extension to ${
+ expectUserAllowed ? "be" : "NOT be"
+ } quarantineIgnoredByUser`
+ );
+ is(
+ radioInputs[0].checked,
+ expectUserAllowed,
+ `Expect 'allow' radio button to ${
+ expectUserAllowed ? "be" : "NOT be"
+ } checked`
+ );
+ is(
+ radioInputs[1].checked,
+ !expectUserAllowed,
+ `Expect 'disallow' radio button ${
+ expectUserAllowed ? "NOT be" : "be"
+ } checked`
+ );
+ };
+
+ info("Verify initially NOT allowed to access quarantine domains");
+ checkRadioInputsState({ expectUserAllowed: false });
+
+ info("Click 'allow' radio input");
+ radioInputs[0].click();
+ checkRadioInputsState({ expectUserAllowed: true });
+
+ info("Click 'disallow' radio input");
+ radioInputs[1].click();
+ checkRadioInputsState({ expectUserAllowed: false });
+
+ info("Verify quarantineIgnoredByUser changes reflected in about:addons UI");
+
+ info("Allow test extension on quarantined domains");
+ let promisePropertyChanged =
+ AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ card.addon.quarantineIgnoredByUser = true;
+ await promisePropertyChanged;
+ checkRadioInputsState({ expectUserAllowed: true });
+
+ info("Disallow test extension on quarantined domains");
+ promisePropertyChanged =
+ AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ card.addon.quarantineIgnoredByUser = false;
+ await promisePropertyChanged;
+ checkRadioInputsState({ expectUserAllowed: false });
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Make sure the quarantined domains feature is initially enabled
+ // otherwise the "quarantineIgnoredByUser UI" rows are
+ // going to be hidden.
+ ["extensions.quarantinedDomains.enabled", true],
+ // Make sure this test is always running with the
+ // "per-addon quarantineIgnoredByUser UI" feature enabled.
+ ["extensions.quarantinedDomains.uiDisabled", false],
+ ],
+ });
+
+ // Clear any per-addon pref once this test file is exiting.
+ registerCleanupFunction(() => {
+ const prefBranch = Services.prefs.getBranch(
+ QuarantinedDomains.PREF_ADDONS_BRANCH_NAME
+ );
+ for (const leafName of prefBranch.getChildList("")) {
+ const prefName = QuarantinedDomains.PREF_ADDONS_BRANCH_NAME + leafName;
+ info(`Clearing user pref ${prefName}`);
+ Services.prefs.clearUserPref(prefName);
+ }
+ });
+
+ await regularExtension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ info("Test quarantineIgnoredByUser UI on a regular extension");
+ let loaded = waitForViewLoad(win);
+ getAddonCard(win, regularExtId).querySelector('[action="expand"]').click();
+ await loaded;
+
+ await testQuarantinedUserAllowedUIRows(regularExtId, { expectVisible: true });
+
+ info("Go back to extensions list view");
+ loaded = waitForViewLoad(win);
+ win.history.back();
+ await loaded;
+
+ info("Test quarantineIgnoredByUser UI on a privileged extension");
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, privilegedExtId).querySelector('[action="expand"]').click();
+ await loaded;
+
+ await testQuarantinedUserAllowedUIRows(privilegedExtId, {
+ expectVisible: false,
+ });
+
+ info("Go back to extensions list view");
+ loaded = waitForViewLoad(win);
+ win.history.back();
+ await loaded;
+
+ info("Test quarantineIgnoredByUser UI on a recommended extension");
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, recommendedExtId)
+ .querySelector('[action="expand"]')
+ .click();
+ await loaded;
+
+ await testQuarantinedUserAllowedUIRows(recommendedExtId, {
+ expectVisible: false,
+ });
+
+ info("Switch to theme list view");
+ loaded = waitForViewLoad(win);
+ doc.querySelector("#categories > [name=theme]").click();
+ await loaded;
+
+ info("Test quarantineIgnoredByUser UI on a non extension addon type (theme)");
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, themeId).querySelector('[action="expand"]').click();
+ await loaded;
+
+ await testQuarantinedUserAllowedUIRows(themeId, { expectVisible: false });
+
+ info("Verify regular extension card on quarantined domains feature disabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.quarantinedDomains.enabled", false]],
+ });
+
+ info("Switch to extension list view");
+ loaded = waitForViewLoad(win);
+ doc.querySelector("#categories > [name=extension]").click();
+ await loaded;
+
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, regularExtId).querySelector('[action="expand"]').click();
+ await loaded;
+
+ await testQuarantinedUserAllowedUIRows(regularExtId, {
+ expectVisible: false,
+ });
+
+ await SpecialPowers.popPrefEnv();
+
+ info("Verify regular extenson card uiDisabled pref set to true");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Make sure the quarantineIgnoredByUser UI is also hidden
+ // when the quarantine domains feature is enabled but the
+ // "per-addon quarantineIgnoredByUser UI" feature is disabled.
+ ["extensions.quarantinedDomains.uiDisabled", true],
+ ],
+ });
+
+ info("Switch to extension list view");
+ loaded = waitForViewLoad(win);
+ doc.querySelector("#categories > [name=extension]").click();
+ await loaded;
+
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, regularExtId).querySelector('[action="expand"]').click();
+ await loaded;
+
+ await testQuarantinedUserAllowedUIRows(regularExtId, {
+ expectVisible: false,
+ });
+
+ await closeView(win);
+ await regularExtension.unload();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testRatingsElementVisibleIfReviewURLExists() {
+ let win = await loadInitialView("extension");
+ let id = "addon4@mochi.test";
+ let card = getAddonCard(win, id);
+
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(win, id);
+
+ let rows = getDetailRows(card);
+
+ let expectedRowCount = 5;
+ if (card.addon.canChangeQuarantineIgnored) {
+ expectedRowCount += 2;
+ }
+ is(rows.length, expectedRowCount, "Expected row count");
+
+ // Reviews.
+ // addon4@mochi.test is similar to addon1@mochi.test whose rows have already
+ // been checked in testFullDetails. Here we only check the last row
+ // which is unique to this test case due to the presence of "reviewURL".
+ let row = rows.pop();
+ await checkLabel(row, "rating");
+ let rating = row.lastElementChild;
+ ok(rating.classList.contains("addon-detail-rating"), "Found the rating el");
+ ok(!row.hidden, "The rating row is shown");
+ let mozFiveStar = rating.querySelector("moz-five-star");
+ is(mozFiveStar.rating, 0, "0 rating when there are no reviews");
+ let stars = Array.from(mozFiveStar.starEls);
+ let fullAttrs = stars.map(star => star.getAttribute("fill")).join(",");
+ is(fullAttrs, "empty,empty,empty,empty,empty", "All stars are empty");
+ let link = rating.querySelector("a");
+ let reviewsLink = formatUrl(
+ "addons-manager-reviews-link",
+ "http://addons.mozilla.org/reviews"
+ );
+ checkLink(link, reviewsLink, {
+ id: "addon-detail-reviews-link",
+ args: { numberOfReviews: 0 },
+ });
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js
new file mode 100644
index 0000000000..bc84ffaf89
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js
@@ -0,0 +1,668 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+loadTestSubscript("head_disco.js");
+
+// The response to the discovery API, as documented at:
+// https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
+//
+// The test is designed to easily verify whether the discopane works with the
+// latest AMO API, by replacing API_RESPONSE_FILE's content with latest AMO API
+// response, e.g. from https://addons.allizom.org/api/v4/discovery/?lang=en-US
+// The response must contain at least one theme, and one extension.
+
+const API_RESPONSE_FILE = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ // Trim empty component from splitting with trailing slash.
+ ...RELATIVE_DIR.split("/").filter(c => c.length),
+ "discovery",
+ "api_response.json"
+);
+
+const AMO_TEST_HOST = "rewritten-for-testing.addons.allizom.org";
+
+const ArrayBufferInputStream = Components.Constructor(
+ "@mozilla.org/io/arraybuffer-input-stream;1",
+ "nsIArrayBufferInputStream",
+ "setData"
+);
+
+const amoServer = AddonTestUtils.createHttpServer({ hosts: [AMO_TEST_HOST] });
+
+amoServer.registerFile(
+ "/png",
+ new FileUtils.File(
+ PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ ...`${RELATIVE_DIR}discovery/small-1x1.png`.split("/")
+ )
+ )
+);
+amoServer.registerPathHandler("/dummy", (request, response) => {
+ response.write("Dummy");
+});
+
+// `result` is an element in the `results` array from AMO's discovery API,
+// stored in API_RESPONSE_FILE.
+function getTestExpectationFromApiResult(result) {
+ return {
+ typeIsTheme: result.addon.type === "statictheme",
+ addonName: result.addon.name,
+ authorName: result.addon.authors[0].name,
+ editorialBody: result.description_text,
+ dailyUsers: result.addon.average_daily_users,
+ rating: result.addon.ratings.average,
+ };
+}
+
+// A helper to declare a response to discovery API requests.
+class DiscoveryAPIHandler {
+ constructor(responseText) {
+ this.setResponseText(responseText);
+ this.requestCount = 0;
+
+ // Overwrite the previous discovery response handler.
+ amoServer.registerPathHandler("/discoapi", this);
+ }
+
+ setResponseText(responseText) {
+ this.responseBody = new TextEncoder().encode(responseText).buffer;
+ }
+
+ // Suspend discovery API requests until unblockResponses is called.
+ blockNextResponses() {
+ this._unblockPromise = new Promise(resolve => {
+ this.unblockResponses = resolve;
+ });
+ }
+
+ unblockResponses(responseText) {
+ throw new Error("You need to call blockNextResponses first!");
+ }
+
+ // nsIHttpRequestHandler::handle
+ async handle(request, response) {
+ ++this.requestCount;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.processAsync();
+ await this._unblockPromise;
+
+ let body = this.responseBody;
+ let binStream = new ArrayBufferInputStream(body, 0, body.byteLength);
+ response.bodyOutputStream.writeFrom(binStream, body.byteLength);
+ response.finish();
+ }
+}
+
+// Retrieve the list of visible action elements inside a document or container.
+function getVisibleActions(documentOrElement) {
+ return Array.from(documentOrElement.querySelectorAll("[action]")).filter(
+ elem =>
+ elem.getAttribute("action") !== "page-options" &&
+ elem.offsetWidth &&
+ elem.offsetHeight
+ );
+}
+
+function getActionName(actionElement) {
+ return actionElement.getAttribute("action");
+}
+
+function getCardByAddonId(win, addonId) {
+ for (let card of win.document.querySelectorAll("recommended-addon-card")) {
+ if (card.addonId === addonId) {
+ return card;
+ }
+ }
+ return null;
+}
+
+// Switch to a different view so we can switch back to the discopane later.
+async function switchToNonDiscoView(win) {
+ // Listeners registered while the discopane was the active view continue to be
+ // active when the view switches to the extensions list, because both views
+ // share the same document.
+ win.gViewController.loadView("addons://list/extension");
+ await wait_for_view_load(win);
+ ok(
+ win.document.querySelector("addon-list"),
+ "Should be at the extension list view"
+ );
+}
+
+// Switch to the discopane and wait until it has fully rendered, including any
+// cards from the discovery API.
+async function switchToDiscoView(win) {
+ is(
+ getDiscoveryElement(win),
+ null,
+ "Cannot switch to discopane when the discopane is already shown"
+ );
+ win.gViewController.loadView("addons://discover/");
+ await wait_for_view_load(win);
+ await promiseDiscopaneUpdate(win);
+}
+
+// Wait until all images in the DOM have successfully loaded.
+// There must be at least one `<img>` in the document.
+// Returns the number of loaded images.
+async function waitForAllImagesLoaded(win) {
+ let imgs = Array.from(
+ win.document.querySelectorAll("discovery-pane img[src]")
+ );
+ function areAllImagesLoaded() {
+ let loadCount = imgs.filter(img => img.naturalWidth).length;
+ info(`Loaded ${loadCount} out of ${imgs.length} images`);
+ return loadCount === imgs.length;
+ }
+ if (!areAllImagesLoaded()) {
+ await promiseEvent(win.document, "load", true, areAllImagesLoaded);
+ }
+ return imgs.length;
+}
+
+// Install an add-on by clicking on the card.
+// The promise resolves once the card has been updated.
+async function testCardInstall(card) {
+ Assert.deepEqual(
+ getVisibleActions(card).map(getActionName),
+ ["install-addon"],
+ "Should have an Install button before install"
+ );
+
+ let installButton =
+ card.querySelector("[data-l10n-id='install-extension-button']") ||
+ card.querySelector("[data-l10n-id='install-theme-button']");
+
+ let updatePromise = promiseEvent(card, "disco-card-updated");
+ installButton.click();
+ await updatePromise;
+
+ Assert.deepEqual(
+ getVisibleActions(card).map(getActionName),
+ ["manage-addon"],
+ "Should have a Manage button after install"
+ );
+}
+
+// Uninstall the add-on (not via the card, since it has no uninstall button).
+// The promise resolves once the card has been updated.
+async function testAddonUninstall(card) {
+ Assert.deepEqual(
+ getVisibleActions(card).map(getActionName),
+ ["manage-addon"],
+ "Should have a Manage button before uninstall"
+ );
+
+ let addon = await AddonManager.getAddonByID(card.addonId);
+
+ let updatePromise = promiseEvent(card, "disco-card-updated");
+ await addon.uninstall();
+ await updatePromise;
+
+ Assert.deepEqual(
+ getVisibleActions(card).map(getActionName),
+ ["install-addon"],
+ "Should have an Install button after uninstall"
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "extensions.getAddons.discovery.api_url",
+ `http://${AMO_TEST_HOST}/discoapi`,
+ ],
+ // Disable non-discopane recommendations to avoid unexpected discovery
+ // API requests.
+ ["extensions.htmlaboutaddons.recommendations.enabled", false],
+ // Disable the telemetry client ID (and its associated UI warning).
+ // browser_html_discover_view_clientid.js covers this functionality.
+ ["browser.discovery.enabled", false],
+ // Disable mixed-content upgrading as this test is expecting an HTTP load
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+});
+
+// Test that the discopane can be loaded and that meaningful results are shown.
+// This relies on response data from the AMO API, stored in API_RESPONSE_FILE.
+add_task(async function discopane_with_real_api_data() {
+ const apiText = await readAPIResponseFixture(
+ AMO_TEST_HOST,
+ API_RESPONSE_FILE
+ );
+ let apiHandler = new DiscoveryAPIHandler(apiText);
+
+ const apiResultArray = JSON.parse(apiText).results;
+ ok(apiResultArray.length, `Mock has ${apiResultArray.length} results`);
+
+ apiHandler.blockNextResponses();
+ let win = await loadInitialView("discover");
+
+ Assert.deepEqual(
+ getVisibleActions(win.document).map(getActionName),
+ [],
+ "The AMO button should be invisible when the AMO API hasn't responded"
+ );
+
+ apiHandler.unblockResponses();
+ await promiseDiscopaneUpdate(win);
+
+ let actionElements = getVisibleActions(win.document);
+ Assert.deepEqual(
+ actionElements.map(getActionName),
+ [
+ // Expecting an install button for every result.
+ ...new Array(apiResultArray.length).fill("install-addon"),
+ "open-amo",
+ ],
+ "All add-on cards should be rendered, with AMO button at the end."
+ );
+
+ let imgCount = await waitForAllImagesLoaded(win);
+ is(imgCount, apiResultArray.length, "Expected an image for every result");
+
+ // Check that the cards have the expected content.
+ let cards = Array.from(
+ win.document.querySelectorAll("recommended-addon-card")
+ );
+ is(cards.length, apiResultArray.length, "Every API result has a card");
+ for (let [i, card] of cards.entries()) {
+ let expectations = getTestExpectationFromApiResult(apiResultArray[i]);
+ info(`Expectations for card ${i}: ${JSON.stringify(expectations)}`);
+
+ let checkContent = (selector, expectation) => {
+ let text = card.querySelector(selector).textContent;
+ is(text, expectation, `Content of selector "${selector}"`);
+ };
+ checkContent(".disco-addon-name", expectations.addonName);
+ await win.document.l10n.translateFragment(card);
+ checkContent(
+ ".disco-addon-author [data-l10n-name='author']",
+ expectations.authorName
+ );
+
+ let amoListingLink = card.querySelector(".disco-addon-author a");
+ ok(
+ amoListingLink.search.includes("utm_source=firefox-browser"),
+ `Listing link should have attribution parameter, url=${amoListingLink}`
+ );
+
+ let actions = getVisibleActions(card);
+ is(actions.length, 1, "Card should only have one install button");
+ let installButton = actions[0];
+ if (expectations.typeIsTheme) {
+ // Theme button + screenshot
+ ok(
+ installButton.matches("[data-l10n-id='install-theme-button'"),
+ "Has theme install button"
+ );
+ ok(
+ card.querySelector(".card-heading-image").offsetWidth,
+ "Preview image must be visible"
+ );
+ } else {
+ // Extension button + extended description.
+ ok(
+ installButton.matches("[data-l10n-id='install-extension-button'"),
+ "Has extension install button"
+ );
+ checkContent(".disco-description-main", expectations.editorialBody);
+
+ let mozFiveStar = card.querySelector("moz-five-star");
+ if (expectations.rating) {
+ is(mozFiveStar.rating, expectations.rating, "Expected rating value");
+ ok(mozFiveStar.offsetWidth, "Rating element is visible");
+ } else {
+ is(mozFiveStar.offsetWidth, 0, "Rating element is not visible");
+ }
+
+ let userCountElem = card.querySelector(".disco-user-count");
+ if (expectations.dailyUsers) {
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(userCountElem),
+ { id: "user-count", args: { dailyUsers: expectations.dailyUsers } },
+ "Card count should be rendered"
+ );
+ } else {
+ is(userCountElem.offsetWidth, 0, "User count element is not visible");
+ }
+ }
+ }
+
+ is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
+
+ await closeView(win);
+});
+
+// Test whether extensions and themes can be installed from the discopane.
+// Also checks that items in the list do not change position after installation,
+// and that they are shown at the bottom of the list when the discopane is
+// reopened.
+add_task(async function install_from_discopane() {
+ const apiText = await readAPIResponseFixture(
+ AMO_TEST_HOST,
+ API_RESPONSE_FILE
+ );
+ const apiResultArray = JSON.parse(apiText).results;
+ let getAddonIdByAMOAddonType = type =>
+ apiResultArray.find(r => r.addon.type === type).addon.guid;
+ const FIRST_EXTENSION_ID = getAddonIdByAMOAddonType("extension");
+ const FIRST_THEME_ID = getAddonIdByAMOAddonType("statictheme");
+
+ let apiHandler = new DiscoveryAPIHandler(apiText);
+
+ let win = await loadInitialView("discover");
+ await promiseDiscopaneUpdate(win);
+ await waitForAllImagesLoaded(win);
+
+ // Test extension install.
+ let installExtensionPromise = promiseAddonInstall(amoServer, {
+ manifest: {
+ name: "My Awesome Add-on",
+ description: "Test extension install button",
+ browser_specific_settings: { gecko: { id: FIRST_EXTENSION_ID } },
+ permissions: ["<all_urls>"],
+ },
+ });
+ await testCardInstall(getCardByAddonId(win, FIRST_EXTENSION_ID));
+ await installExtensionPromise;
+
+ // Test theme install.
+ let installThemePromise = promiseAddonInstall(amoServer, {
+ manifest: {
+ name: "My Fancy Theme",
+ description: "Test theme install button",
+ browser_specific_settings: { gecko: { id: FIRST_THEME_ID } },
+ theme: {
+ colors: {
+ tab_selected: "red",
+ },
+ },
+ },
+ });
+ let promiseThemeChange = promiseObserved("lightweight-theme-styling-update");
+ await testCardInstall(getCardByAddonId(win, FIRST_THEME_ID));
+ await installThemePromise;
+ await promiseThemeChange;
+
+ // After installing, the cards should have manage buttons instead of install
+ // buttons. The cards should still be at the top of the pane (and not be
+ // moved to the bottom).
+ Assert.deepEqual(
+ getVisibleActions(win.document).map(getActionName),
+ [
+ "manage-addon",
+ "manage-addon",
+ ...new Array(apiResultArray.length - 2).fill("install-addon"),
+ "open-amo",
+ ],
+ "The Install buttons should be replaced with Manage buttons"
+ );
+
+ // End of the testing installation from a card.
+
+ // Click on the Manage button to verify that it does something useful,
+ // and in order to be able to force the discovery pane to be rendered again.
+ let loaded = waitForViewLoad(win);
+ getCardByAddonId(win, FIRST_EXTENSION_ID)
+ .querySelector("[action='manage-addon']")
+ .click();
+ await loaded;
+ {
+ let addonCard = win.document.querySelector(
+ `addon-card[addon-id="${FIRST_EXTENSION_ID}"]`
+ );
+ ok(addonCard, "Add-on details should be shown");
+ ok(addonCard.expanded, "The card should have been expanded");
+ // TODO bug 1540253: Check that the "recommended" badge is visible.
+ }
+
+ // Now we are going to force an updated rendering and check that the cards are
+ // in the expected order, and then test uninstallation of the above add-ons.
+ await switchToDiscoView(win);
+ await waitForAllImagesLoaded(win);
+
+ Assert.deepEqual(
+ getVisibleActions(win.document).map(getActionName),
+ [
+ ...new Array(apiResultArray.length - 2).fill("install-addon"),
+ "manage-addon",
+ "manage-addon",
+ "open-amo",
+ ],
+ "Already-installed add-ons should be rendered at the end of the list"
+ );
+
+ promiseThemeChange = promiseObserved("lightweight-theme-styling-update");
+ await testAddonUninstall(getCardByAddonId(win, FIRST_THEME_ID));
+ await promiseThemeChange;
+ await testAddonUninstall(getCardByAddonId(win, FIRST_EXTENSION_ID));
+
+ is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
+
+ await closeView(win);
+});
+
+// Tests that the page is able to switch views while the discopane is loading,
+// without inadvertently replacing the page when the request finishes.
+add_task(async function discopane_navigate_while_loading() {
+ let apiHandler = new DiscoveryAPIHandler(`{"results": []}`);
+
+ apiHandler.blockNextResponses();
+ let win = await loadInitialView("discover");
+
+ let updatePromise = promiseDiscopaneUpdate(win);
+ let didUpdateDiscopane = false;
+ updatePromise.then(() => {
+ didUpdateDiscopane = true;
+ });
+
+ // Switch views while the request is pending.
+ await switchToNonDiscoView(win);
+
+ is(
+ didUpdateDiscopane,
+ false,
+ "discopane should still not be updated because the request is blocked"
+ );
+ is(
+ getDiscoveryElement(win),
+ null,
+ "Discopane should be removed after switching to the extension list"
+ );
+
+ // Release pending requests, to verify that completing the request will not
+ // cause changes to the visible view. The updatePromise will still resolve
+ // though, because the event is dispatched to the removed `<discovery-pane>`.
+ apiHandler.unblockResponses();
+
+ await updatePromise;
+ ok(
+ win.document.querySelector("addon-list"),
+ "Should still be at the extension list view"
+ );
+ is(
+ getDiscoveryElement(win),
+ null,
+ "Discopane should not be in the document when it is not the active view"
+ );
+
+ is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
+
+ await closeView(win);
+});
+
+// Tests that invalid responses are handled correctly and not cached.
+// Also verifies that the response is cached as long as the page is active,
+// but not when the page is fully reloaded.
+add_task(async function discopane_cache_api_responses() {
+ const INVALID_RESPONSE_BODY = `{"This is some": invalid} JSON`;
+ let apiHandler = new DiscoveryAPIHandler(INVALID_RESPONSE_BODY);
+
+ let expectedErrMsg;
+ try {
+ JSON.parse(INVALID_RESPONSE_BODY);
+ ok(false, "JSON.parse should have thrown");
+ } catch (e) {
+ expectedErrMsg = e.message;
+ }
+
+ let invalidResponseHandledPromise = new Promise(resolve => {
+ Services.console.registerListener(function listener(msg) {
+ if (msg.message.includes(expectedErrMsg)) {
+ resolve();
+ Services.console.unregisterListener(listener);
+ }
+ });
+ });
+
+ let win = await loadInitialView("discover"); // Request #1
+ await promiseDiscopaneUpdate(win);
+
+ info("Waiting for expected error");
+ await invalidResponseHandledPromise;
+ is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
+
+ Assert.deepEqual(
+ getVisibleActions(win.document).map(getActionName),
+ ["open-amo"],
+ "The AMO button should be visible even when the response was invalid"
+ );
+
+ // Change to a valid response, so that the next response will be cached.
+ apiHandler.setResponseText(`{"results": []}`);
+
+ await switchToNonDiscoView(win);
+ await switchToDiscoView(win); // Request #2
+
+ is(
+ apiHandler.requestCount,
+ 2,
+ "Should fetch new data because an invalid response should not be cached"
+ );
+
+ await switchToNonDiscoView(win);
+ await switchToDiscoView(win);
+ await closeView(win);
+
+ is(
+ apiHandler.requestCount,
+ 2,
+ "The previous response was valid and should have been reused"
+ );
+
+ // Now open a new about:addons page and verify that a new API request is sent.
+ let anotherWin = await loadInitialView("discover");
+ await promiseDiscopaneUpdate(anotherWin);
+ await closeView(anotherWin);
+
+ is(apiHandler.requestCount, 3, "discovery API should be requested again");
+});
+
+add_task(async function discopane_no_cookies() {
+ let requestPromise = new Promise(resolve => {
+ amoServer.registerPathHandler("/discoapi", resolve);
+ });
+ Services.cookies.add(
+ AMO_TEST_HOST,
+ "/",
+ "name",
+ "value",
+ false,
+ false,
+ false,
+ Date.now() / 1000 + 600,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTP
+ );
+ let win = await loadInitialView("discover");
+ let request = await requestPromise;
+ ok(!request.hasHeader("Cookie"), "discovery API should not receive cookies");
+ await closeView(win);
+});
+
+// The CSP of about:addons whitelists http:, but not data:, hence we are
+// loading a little red data: image which gets blocked by the CSP.
+add_task(async function csp_img_src() {
+ const RED_DATA_IMAGE =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAA" +
+ "AHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+ // Minimal API response to get the image in recommended-addon-card to render.
+ const DUMMY_EXTENSION_ID = "dummy-csp@extensionid";
+ const apiResponse = {
+ results: [
+ {
+ addon: {
+ guid: DUMMY_EXTENSION_ID,
+ type: "extension",
+ authors: [
+ {
+ name: "Some CSP author",
+ },
+ ],
+ url: `http://${AMO_TEST_HOST}/dummy`,
+ icon_url: RED_DATA_IMAGE,
+ },
+ },
+ ],
+ };
+
+ let apiHandler = new DiscoveryAPIHandler(JSON.stringify(apiResponse));
+ apiHandler.blockNextResponses();
+ let win = await loadInitialView("discover");
+
+ let cspPromise = new Promise(resolve => {
+ win.addEventListener("securitypolicyviolation", e => {
+ // non http(s) loads only report the scheme
+ is(e.blockedURI, "data", "CSP: blocked URI");
+ is(e.violatedDirective, "img-src", "CSP: violated directive");
+ resolve();
+ });
+ });
+
+ apiHandler.unblockResponses();
+ await cspPromise;
+
+ await closeView(win);
+});
+
+add_task(async function checkDiscopaneNotice() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.discovery.enabled", true],
+ // Enabling the Data Upload pref may upload data.
+ // Point data reporting services to localhost so the data doesn't escape.
+ ["toolkit.telemetry.server", "https://localhost:1337"],
+ ["telemetry.fog.test.localhost_port", -1],
+ ["datareporting.healthreport.uploadEnabled", true],
+ ["extensions.htmlaboutaddons.recommendations.enabled", true],
+ ["extensions.recommendations.hideNotice", false],
+ // Disable mixed-content upgrading as this test is expecting an HTTP load
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+
+ let win = await loadInitialView("extension");
+ let messageBar = win.document.querySelector(
+ "moz-message-bar.discopane-notice"
+ );
+ ok(messageBar, "Recommended notice should exist in extensions view");
+ await switchToDiscoView(win);
+ messageBar = win.document.querySelector("moz-message-bar.discopane-notice");
+ ok(messageBar, "Recommended notice should exist in disco view");
+
+ messageBar.closeButtonEl.click();
+ messageBar = win.document.querySelector("moz-message-bar.discopane-notice");
+ ok(!messageBar, "Recommended notice should not exist in disco view");
+ await switchToNonDiscoView(win);
+ messageBar = win.document.querySelector("moz-message-bar.discopane-notice");
+ ok(!messageBar, "Recommended notice should not exist in extensions view");
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js
new file mode 100644
index 0000000000..ff95c88fe1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js
@@ -0,0 +1,219 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+const server = AddonTestUtils.createHttpServer();
+const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`;
+server.registerPathHandler("/sumo/personalized-addons", (request, response) => {
+ response.write("This is a SUMO page that explains personalized add-ons.");
+});
+
+// Before a discovery API request is triggered, this method should be called.
+// Resolves with the value of the "telemetry-client-id" query parameter.
+async function promiseOneDiscoveryApiRequest() {
+ return new Promise(resolve => {
+ let requestCount = 0;
+ // Overwrite previous request handler, if any.
+ server.registerPathHandler("/discoapi", (request, response) => {
+ is(++requestCount, 1, "Expecting one discovery API request");
+ response.write(`{"results": []}`);
+ let searchParams = new URLSearchParams(request.queryString);
+ let clientId = searchParams.get("telemetry-client-id");
+ resolve(clientId);
+ });
+ });
+}
+
+function getNoticeButton(win) {
+ return win.document.querySelector("[action='notice-learn-more']");
+}
+
+function isNoticeVisible(win) {
+ let message = win.document.querySelector("taar-notice");
+ return message && message.offsetHeight > 0;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Enable clientid - see Discovery.sys.mjs for the first two prefs.
+ ["browser.discovery.enabled", true],
+ // Enabling the Data Upload pref may upload data.
+ // Point data reporting services to localhost so the data doesn't escape.
+ ["toolkit.telemetry.server", "https://localhost:1337"],
+ ["telemetry.fog.test.localhost_port", -1],
+ ["datareporting.healthreport.uploadEnabled", true],
+ ["extensions.getAddons.discovery.api_url", `${serverBaseUrl}discoapi`],
+ ["app.support.baseURL", `${serverBaseUrl}sumo/`],
+ // Discovery API requests can be triggered by the discopane and the
+ // recommendations in the list view. To make sure that the every test
+ // checks the behavior of the view they're testing, ensure that only one
+ // of the two views is enabled at a time.
+ ["extensions.htmlaboutaddons.recommendations.enabled", false],
+ ],
+ });
+});
+
+// Test that the clientid is passed to the API when enabled via prefs.
+add_task(async function clientid_enabled() {
+ let EXPECTED_CLIENT_ID = await ClientID.getClientIdHash();
+ ok(EXPECTED_CLIENT_ID, "ClientID should be available");
+
+ let requestPromise = promiseOneDiscoveryApiRequest();
+ let win = await loadInitialView("discover");
+
+ ok(isNoticeVisible(win), "Notice about personalization should be visible");
+
+ // TODO: This should ideally check whether the result is the expected ID.
+ // But run with --verify, the test may fail with EXPECTED_CLIENT_ID being
+ // "baae8d197cf6b0865d7ba7ddf83829cd2d9844374d7271a5c704199d91059316",
+ // which is sha256(TelemetryUtils.knownClientId).
+ // This happens because at the end of the test, the pushPrefEnv from setup is
+ // reverted, which resets datareporting.healthreport.uploadEnabled to false.
+ // When TelemetryController.sys.mjs detects this, it asynchronously resets the
+ // ClientID to knownClientId - which may happen at the next run of the test.
+ // TODO: Fix this together with bug 1537933
+ //
+ // is(await requestPromise, EXPECTED_CLIENT_ID,
+ ok(
+ await requestPromise,
+ "Moz-Client-Id should be set when telemetry & discovery are enabled"
+ );
+
+ let tabbrowser = win.windowRoot.ownerGlobal.gBrowser;
+ let expectedUrl = `${serverBaseUrl}sumo/personalized-addons`;
+ let tabPromise = BrowserTestUtils.waitForNewTab(tabbrowser, expectedUrl);
+
+ getNoticeButton(win).click();
+
+ info(`Waiting for new tab with URL: ${expectedUrl}`);
+ let tab = await tabPromise;
+ BrowserTestUtils.removeTab(tab);
+
+ await closeView(win);
+});
+
+// Test that the clientid is not sent when disabled via prefs.
+add_task(async function clientid_disabled() {
+ // Temporarily override the prefs that we had set in setup.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.discovery.enabled", false]],
+ });
+ let requestPromise = promiseOneDiscoveryApiRequest();
+ let win = await loadInitialView("discover");
+ ok(!isNoticeVisible(win), "Notice about personalization should be hidden");
+ is(
+ await requestPromise,
+ null,
+ "Moz-Client-Id should not be sent when discovery is disabled"
+ );
+ await closeView(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test that the clientid is not sent from private windows.
+add_task(async function clientid_from_private_window() {
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let requestPromise = promiseOneDiscoveryApiRequest();
+ let managerWindow = await open_manager(
+ "addons://discover/",
+ null,
+ null,
+ null,
+ privateWindow
+ );
+ ok(
+ PrivateBrowsingUtils.isContentWindowPrivate(managerWindow),
+ "Addon-manager is in a private window"
+ );
+
+ is(
+ await requestPromise,
+ null,
+ "Moz-Client-Id should not be sent in private windows"
+ );
+
+ await close_manager(managerWindow);
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+add_task(async function clientid_enabled_from_extension_list() {
+ await SpecialPowers.pushPrefEnv({
+ // Override prefs from setup to enable recommendations.
+ set: [
+ ["extensions.htmlaboutaddons.recommendations.enabled", true],
+ ["extensions.getAddons.showPane", false],
+ ],
+ });
+
+ // Force the extension list to be the first load. This pref will be
+ // overwritten once the view loads.
+ Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/extension");
+
+ let requestPromise = promiseOneDiscoveryApiRequest();
+ let win = await loadInitialView("extension");
+
+ ok(isNoticeVisible(win), "Notice about personalization should be visible");
+
+ ok(
+ await requestPromise,
+ "Moz-Client-Id should be set when telemetry & discovery are enabled"
+ );
+
+ // Make sure switching to the theme view doesn't trigger another request.
+ await switchView(win, "theme");
+
+ // Wait until the request would have happened so promiseOneDiscoveryApiRequest
+ // can fail if it does.
+ let recommendations = win.document.querySelector("recommended-addon-list");
+ await recommendations.loadCardsIfNeeded();
+
+ await closeView(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function clientid_enabled_from_theme_list() {
+ await SpecialPowers.pushPrefEnv({
+ // Override prefs from setup to enable recommendations.
+ set: [
+ ["extensions.htmlaboutaddons.recommendations.enabled", true],
+ ["extensions.getAddons.showPane", false],
+ ],
+ });
+
+ // Force the theme list to be the first load. This pref will be overwritten
+ // once the view loads.
+ Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/theme");
+
+ let requestPromise = promiseOneDiscoveryApiRequest();
+ let win = await loadInitialView("theme");
+
+ ok(!isNoticeVisible(win), "Notice about personalization should be hidden");
+
+ is(
+ await requestPromise,
+ null,
+ "Moz-Client-Id should not be sent when loading themes initially"
+ );
+
+ info("Load the extension list and verify the client ID is now sent");
+
+ requestPromise = promiseOneDiscoveryApiRequest();
+ await switchView(win, "extension");
+
+ ok(await requestPromise, "Moz-Client-Id is now sent for extensions");
+
+ await closeView(win);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js
new file mode 100644
index 0000000000..474cd424b9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js
@@ -0,0 +1,83 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+const server = AddonTestUtils.createHttpServer();
+const TEST_API_URL = `http://localhost:${server.identity.primaryPort}/discoapi`;
+
+async function checkIfDiscoverVisible(expectVisible) {
+ let requestCount = 0;
+ let requestPromise = new Promise(resolve => {
+ // Overwrites previous request handler, if any.
+ server.registerPathHandler("/discoapi", (request, response) => {
+ ++requestCount;
+ response.write(`{"results": []}`);
+ resolve();
+ });
+ });
+
+ // Open about:addons with default view.
+ let managerWindow = await open_manager(null);
+ let categoryUtilities = new CategoryUtilities(managerWindow);
+
+ is(
+ categoryUtilities.isTypeVisible("discover"),
+ expectVisible,
+ "Visibility of discopane"
+ );
+
+ await wait_for_view_load(managerWindow);
+ if (expectVisible) {
+ is(
+ categoryUtilities.selectedCategory,
+ "discover",
+ "Expected discopane as the default view"
+ );
+ await requestPromise;
+ is(requestCount, 1, "Expected discovery API request");
+ } else {
+ // The next view (after discopane) is the extension list.
+ is(
+ categoryUtilities.selectedCategory,
+ "extension",
+ "Should fall back to another view when the discopane is disabled"
+ );
+ is(requestCount, 0, "Discovery API should not be requested");
+ }
+
+ await close_manager(managerWindow);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.getAddons.discovery.api_url", TEST_API_URL],
+ // Disable recommendations at the HTML about:addons view to avoid sending
+ // a discovery API request from the fallback view (extension list) in the
+ // showPane_false test.
+ ["extensions.htmlaboutaddons.recommendations.enabled", false],
+ ],
+ });
+});
+
+add_task(async function showPane_true() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DISCOVER_ENABLED, true]],
+ clear: [[PREF_UI_LASTCATEGORY]],
+ });
+ await checkIfDiscoverVisible(true);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function showPane_false() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DISCOVER_ENABLED, false]],
+ clear: [[PREF_UI_LASTCATEGORY]],
+ });
+ await checkIfDiscoverVisible(false);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
new file mode 100644
index 0000000000..2631a164df
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
@@ -0,0 +1,1063 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+let promptService;
+
+const SUPPORT_URL = Services.urlFormatter.formatURL(
+ Services.prefs.getStringPref("app.support.baseURL")
+);
+const REMOVE_SUMO_URL = SUPPORT_URL + "cant-remove-addon";
+
+function getTestCards(root) {
+ return root.querySelectorAll('addon-card[addon-id$="@mochi.test"]');
+}
+
+function getCardByAddonId(root, id) {
+ return root.querySelector(`addon-card[addon-id="${id}"]`);
+}
+
+function isEmpty(el) {
+ return !el.children.length;
+}
+
+function waitForThemeChange(list) {
+ // Wait for two move events. One theme will be enabled and another disabled.
+ let moveCount = 0;
+ return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2);
+}
+
+let mockProvider;
+
+add_setup(async function () {
+ mockProvider = new MockProvider(["extension", "sitepermission"]);
+ promptService = mockPromptService();
+});
+
+let extensionsCreated = 0;
+
+function createExtensions(manifestExtras) {
+ return manifestExtras.map(extra =>
+ ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension",
+ browser_specific_settings: {
+ gecko: { id: `test-${extensionsCreated++}@mochi.test` },
+ },
+ icons: {
+ 32: "test-icon.png",
+ },
+ ...extra,
+ },
+ useAddonManager: "temporary",
+ })
+ );
+}
+
+add_task(async function testExtensionList() {
+ let id = "test@mochi.test";
+ let headingId = "test_mochi_test-heading";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension",
+ browser_specific_settings: { gecko: { id } },
+ icons: {
+ 32: "test-icon.png",
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let addon = await AddonManager.getAddonByID(id);
+ ok(addon, "The add-on can be found");
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Find the addon-list to listen for events.
+ let list = doc.querySelector("addon-list");
+
+ // There shouldn't be any disabled extensions.
+ let disabledSection = getSection(doc, "extension-disabled-section");
+ ok(isEmpty(disabledSection), "The disabled section is empty");
+
+ // The loaded extension should be in the enabled list.
+ let enabledSection = getSection(doc, "extension-enabled-section");
+ ok(
+ enabledSection && !isEmpty(enabledSection),
+ "The enabled section isn't empty"
+ );
+ let card = getCardByAddonId(enabledSection, id);
+ ok(card, "The card is in the enabled section");
+
+ // Check the properties of the card.
+ is(card.addonNameEl.textContent, "Test extension", "The name is set");
+ is(
+ card.querySelector("h3").id,
+ headingId,
+ "The add-on name has the correct id"
+ );
+ is(
+ card.querySelector(".card").getAttribute("aria-labelledby"),
+ headingId,
+ "The card is labelled by the heading"
+ );
+ let icon = card.querySelector(".addon-icon");
+ ok(icon.src.endsWith("/test-icon.png"), "The icon is set");
+
+ // Disable the extension.
+ let disableToggle = card.querySelector('[action="toggle-disabled"]');
+ ok(disableToggle.pressed, "The disable toggle is pressed");
+ is(
+ doc.l10n.getAttributes(disableToggle).id,
+ "extension-enable-addon-button-label",
+ "The toggle has the enable label"
+ );
+ ok(disableToggle.getAttribute("aria-label"), "There's an aria-label");
+ ok(!disableToggle.hidden, "The toggle is visible");
+
+ let disabled = BrowserTestUtils.waitForEvent(list, "move");
+ disableToggle.click();
+ await disabled;
+ is(
+ card.parentNode,
+ disabledSection,
+ "The card is now in the disabled section"
+ );
+
+ // The disable button is now enabled.
+ ok(!disableToggle.pressed, "The disable toggle is not pressed");
+ is(
+ doc.l10n.getAttributes(disableToggle).id,
+ "extension-enable-addon-button-label",
+ "The button has the same enable label"
+ );
+ ok(disableToggle.getAttribute("aria-label"), "There's an aria-label");
+
+ // Remove the add-on.
+ let removeButton = card.querySelector('[action="remove"]');
+ is(
+ doc.l10n.getAttributes(removeButton).id,
+ "remove-addon-button",
+ "The button has the remove label"
+ );
+ // There is a support link when the add-on isn't removeable, verify we don't
+ // always include one.
+ ok(!removeButton.querySelector("a"), "There isn't a link in the item");
+
+ // Remove but cancel.
+ let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled");
+ removeButton.click();
+ await cancelled;
+
+ let removed = BrowserTestUtils.waitForEvent(list, "remove");
+ // Tell the mock prompt service that the prompt was accepted.
+ promptService._response = 0;
+ removeButton.click();
+ await removed;
+
+ addon = await AddonManager.getAddonByID(id);
+ ok(
+ addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "The addon is pending uninstall"
+ );
+
+ // Ensure that a pending uninstall bar has been created for the
+ // pending uninstall extension, and pressing the undo button will
+ // refresh the list and render a card to the re-enabled extension.
+ assertHasPendingUninstalls(list, 1);
+ assertHasPendingUninstallAddon(list, addon);
+
+ // Add a second pending uninstall extension.
+ info("Install a second test extension and wait for addon card rendered");
+ let added = BrowserTestUtils.waitForEvent(list, "add");
+ const extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension 2",
+ browser_specific_settings: { gecko: { id: "test-2@mochi.test" } },
+ icons: {
+ 32: "test-icon.png",
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension2.startup();
+
+ await added;
+ ok(
+ getCardByAddonId(list, extension2.id),
+ "Got a card added for the second extension"
+ );
+
+ info("Uninstall the second test extension and wait for addon card removed");
+ removed = BrowserTestUtils.waitForEvent(list, "remove");
+ const addon2 = await AddonManager.getAddonByID(extension2.id);
+ addon2.uninstall(true);
+ await removed;
+
+ ok(
+ !getCardByAddonId(list, extension2.id),
+ "Addon card for the second extension removed"
+ );
+
+ assertHasPendingUninstalls(list, 2);
+ assertHasPendingUninstallAddon(list, addon2);
+
+ // Addon2 was enabled before entering the pending uninstall state,
+ // wait for its startup after pressing undo.
+ let addon2Started = AddonTestUtils.promiseWebExtensionStartup(addon2.id);
+ await testUndoPendingUninstall(list, addon);
+ await testUndoPendingUninstall(list, addon2);
+ info("Wait for the second pending uninstal add-ons startup");
+ await addon2Started;
+
+ ok(
+ getCardByAddonId(disabledSection, addon.id),
+ "The card for the first extension is in the disabled section"
+ );
+ ok(
+ getCardByAddonId(enabledSection, addon2.id),
+ "The card for the second extension is in the enabled section"
+ );
+
+ await extension2.unload();
+ await extension.unload();
+
+ // Install a theme and verify that it is not listed in the pending
+ // uninstall message bars while the list extensions view is loaded.
+ const themeXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ name: "My theme",
+ browser_specific_settings: { gecko: { id: "theme@mochi.test" } },
+ theme: {},
+ },
+ });
+ const themeAddon = await AddonManager.installTemporaryAddon(themeXpi);
+ // Leave it pending uninstall, the following assertions related to
+ // the pending uninstall message bars will fail if the theme is listed.
+ await themeAddon.uninstall(true);
+
+ // Install a third addon to verify that is being fully removed once the
+ // about:addons page is closed.
+ const xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ name: "Test extension 3",
+ browser_specific_settings: { gecko: { id: "test-3@mochi.test" } },
+ icons: {
+ 32: "test-icon.png",
+ },
+ },
+ });
+
+ added = BrowserTestUtils.waitForEvent(list, "add");
+ const addon3 = await AddonManager.installTemporaryAddon(xpi);
+ await added;
+ ok(
+ getCardByAddonId(list, addon3.id),
+ "Addon card for the third extension added"
+ );
+
+ removed = BrowserTestUtils.waitForEvent(list, "remove");
+ addon3.uninstall(true);
+ await removed;
+ ok(
+ !getCardByAddonId(list, addon3.id),
+ "Addon card for the third extension removed"
+ );
+
+ assertHasPendingUninstalls(list, 1);
+ ok(
+ addon3 && !!(addon3.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "The third addon is pending uninstall"
+ );
+
+ await closeView(win);
+
+ ok(
+ !(await AddonManager.getAddonByID(addon3.id)),
+ "The third addon has been fully uninstalled"
+ );
+
+ ok(
+ themeAddon.pendingOperations & AddonManager.PENDING_UNINSTALL,
+ "The theme addon is pending after the list extension view is closed"
+ );
+
+ await themeAddon.uninstall();
+
+ ok(
+ !(await AddonManager.getAddonByID(themeAddon.id)),
+ "The theme addon is fully uninstalled"
+ );
+});
+
+add_task(async function testMouseSupport() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension",
+ browser_specific_settings: { gecko: { id: "test@mochi.test" } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let [card] = getTestCards(doc);
+ is(card.addon.id, "test@mochi.test", "The right card is found");
+
+ let panel = card.querySelector("panel-list");
+
+ ok(!panel.open, "The panel is initially closed");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "addon-card[addon-id$='@mochi.test'] button[action='more-options']",
+ { type: "mousedown" },
+ win.docShell.browsingContext
+ );
+ ok(panel.open, "The panel is now open");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testKeyboardSupport() {
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Some helpers.
+ let tab = event => EventUtils.synthesizeKey("VK_TAB", event);
+ let space = () => EventUtils.synthesizeKey(" ", {});
+ let isFocused = (el, msg) => is(doc.activeElement, el, msg);
+
+ // Find the addon-list to listen for events.
+ let list = doc.querySelector("addon-list");
+ let enabledSection = getSection(doc, "extension-enabled-section");
+ let disabledSection = getSection(doc, "extension-disabled-section");
+
+ // Find the card.
+ let [card] = getTestCards(list);
+ is(card.addon.id, "test@mochi.test", "The right card is found");
+
+ // Focus the more options menu button.
+ let moreOptionsButton = card.querySelector('[action="more-options"]');
+ moreOptionsButton.focus();
+ isFocused(moreOptionsButton, "The more options button is focused");
+
+ // Test opening and closing the menu.
+ let moreOptionsMenu = card.querySelector("panel-list");
+ let expandButton = moreOptionsMenu.querySelector('[action="expand"]');
+ let removeButton = card.querySelector('[action="remove"]');
+ is(moreOptionsMenu.open, false, "The menu is closed");
+ let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
+ space();
+ await shown;
+ is(moreOptionsMenu.open, true, "The menu is open");
+ isFocused(removeButton, "The remove button is now focused");
+ tab({ shiftKey: true });
+ is(moreOptionsMenu.open, true, "The menu stays open");
+ isFocused(expandButton, "The focus has looped to the bottom");
+ tab();
+ is(moreOptionsMenu.open, true, "The menu stays open");
+ isFocused(removeButton, "The focus has looped to the top");
+
+ let hidden = BrowserTestUtils.waitForEvent(moreOptionsMenu, "hidden");
+ EventUtils.synthesizeKey("Escape", {});
+ await hidden;
+ isFocused(moreOptionsButton, "Escape closed the menu");
+
+ // Disable the add-on.
+ let disableButton = card.querySelector('[action="toggle-disabled"]');
+ tab({ shiftKey: true });
+ isFocused(disableButton, "The disable toggle is focused");
+ is(card.parentNode, enabledSection, "The card is in the enabled section");
+ space();
+ // Wait for the add-on state to change.
+ let [disabledAddon] = await AddonTestUtils.promiseAddonEvent("onDisabled");
+ is(disabledAddon.id, id, "The right add-on was disabled");
+ is(
+ card.parentNode,
+ enabledSection,
+ "The card is still in the enabled section"
+ );
+ isFocused(disableButton, "The disable button is still focused");
+ let moved = BrowserTestUtils.waitForEvent(list, "move");
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to clear the focused
+ // state with a mouse which can be done by assistive technology and keyboard
+ // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ // Click outside the list to clear any focus.
+ EventUtils.synthesizeMouseAtCenter(
+ doc.querySelector(".header-name"),
+ {},
+ win
+ );
+ AccessibilityUtils.resetEnv();
+ await moved;
+ is(
+ card.parentNode,
+ disabledSection,
+ "The card moved when keyboard focus left the list"
+ );
+
+ // Remove the add-on.
+ moreOptionsButton.focus();
+ shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
+ space();
+ is(moreOptionsMenu.open, true, "The menu is open");
+ await shown;
+ isFocused(removeButton, "The remove button is focused");
+ let removed = BrowserTestUtils.waitForEvent(list, "remove");
+ space();
+ await removed;
+ is(card.parentNode, null, "The card is no longer on the page");
+
+ await extension.unload();
+ await closeView(win);
+});
+
+add_task(async function testOpenDetailFromNameKeyboard() {
+ let id = "details@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Detail extension",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+
+ let card = getCardByAddonId(win.document, id);
+
+ info("focus the add-on's name, which should be an <a>");
+ card.addonNameEl.focus();
+
+ let detailsLoaded = waitForViewLoad(win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await detailsLoaded;
+
+ card = getCardByAddonId(win.document, id);
+ is(
+ card.addonNameEl.textContent,
+ "Detail extension",
+ "The right detail view is laoded"
+ );
+
+ await extension.unload();
+ await closeView(win);
+});
+
+add_task(async function testExtensionReordering() {
+ let extensions = createExtensions([
+ { name: "Extension One" },
+ { name: "This is last" },
+ { name: "An extension, is first" },
+ ]);
+
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Get a reference to the addon-list for events.
+ let list = doc.querySelector("addon-list");
+
+ // Find the related cards, they should all have @mochi.test ids.
+ let enabledSection = getSection(doc, "extension-enabled-section");
+ let cards = getTestCards(enabledSection);
+
+ is(cards.length, 3, "Each extension has an addon-card");
+
+ let order = Array.from(cards).map(card => card.addon.name);
+ Assert.deepEqual(
+ order,
+ ["An extension, is first", "Extension One", "This is last"],
+ "The add-ons are sorted by name"
+ );
+
+ // Disable the second extension.
+ let disabledSection = getSection(doc, "extension-disabled-section");
+ ok(isEmpty(disabledSection), "The disabled section is initially empty");
+
+ // Disable the add-ons in a different order.
+ let reorderedCards = [cards[1], cards[0], cards[2]];
+ for (let { addon } of reorderedCards) {
+ let moved = BrowserTestUtils.waitForEvent(list, "move");
+ await addon.disable();
+ await moved;
+ }
+
+ order = Array.from(getTestCards(disabledSection)).map(
+ card => card.addon.name
+ );
+ Assert.deepEqual(
+ order,
+ ["An extension, is first", "Extension One", "This is last"],
+ "The add-ons are sorted by name"
+ );
+
+ // All of our installed add-ons are disabled, install a new one.
+ let [newExtension] = createExtensions([{ name: "Extension New" }]);
+ let added = BrowserTestUtils.waitForEvent(list, "add");
+ await newExtension.startup();
+ await added;
+
+ let [newCard] = getTestCards(enabledSection);
+ is(
+ newCard.addon.name,
+ "Extension New",
+ "The new add-on is in the enabled list"
+ );
+
+ // Enable everything again.
+ for (let { addon } of cards) {
+ let moved = BrowserTestUtils.waitForEvent(list, "move");
+ await addon.enable();
+ await moved;
+ }
+
+ order = Array.from(getTestCards(enabledSection)).map(card => card.addon.name);
+ Assert.deepEqual(
+ order,
+ [
+ "An extension, is first",
+ "Extension New",
+ "Extension One",
+ "This is last",
+ ],
+ "The add-ons are sorted by name"
+ );
+
+ // Remove the new extension.
+ let removed = BrowserTestUtils.waitForEvent(list, "remove");
+ await newExtension.unload();
+ await removed;
+ is(newCard.parentNode, null, "The new card has been removed");
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+ await closeView(win);
+});
+
+add_task(async function testThemeList() {
+ let theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "theme@mochi.test" } },
+ name: "My theme",
+ theme: {},
+ },
+ useAddonManager: "temporary",
+ });
+
+ let win = await loadInitialView("theme");
+ let doc = win.document;
+
+ let list = doc.querySelector("addon-list");
+
+ let cards = getTestCards(list);
+ is(cards.length, 0, "There are no test themes to start");
+
+ let added = BrowserTestUtils.waitForEvent(list, "add");
+ await theme.startup();
+ await added;
+
+ cards = getTestCards(list);
+ is(cards.length, 1, "There is now one custom theme");
+
+ let [card] = cards;
+ is(card.addon.name, "My theme", "The card is for the test theme");
+
+ let enabledSection = getSection(doc, "theme-enabled-section");
+ let disabledSection = getSection(doc, "theme-disabled-section");
+
+ await TestUtils.waitForCondition(
+ () => enabledSection.querySelectorAll("addon-card").length == 1
+ );
+
+ is(
+ card.parentNode,
+ enabledSection,
+ "The new theme card is in the enabled section"
+ );
+ is(
+ enabledSection.querySelectorAll("addon-card").length,
+ 1,
+ "There is one enabled theme"
+ );
+
+ let toggleThemeEnabled = async () => {
+ let themesChanged = waitForThemeChange(list);
+ card.querySelector('[action="toggle-disabled"]').click();
+ await themesChanged;
+
+ await TestUtils.waitForCondition(
+ () => enabledSection.querySelectorAll("addon-card").length == 1
+ );
+ };
+
+ await toggleThemeEnabled();
+
+ is(
+ card.parentNode,
+ disabledSection,
+ "The card is now in the disabled section"
+ );
+ is(
+ enabledSection.querySelectorAll("addon-card").length,
+ 1,
+ "There is one enabled theme"
+ );
+
+ // Re-enable the theme.
+ await toggleThemeEnabled();
+ is(card.parentNode, enabledSection, "Card is back in the Enabled section");
+
+ // Remove theme and verify that the default theme is re-enabled.
+ let removed = BrowserTestUtils.waitForEvent(list, "remove");
+ // Confirm removal.
+ promptService._response = 0;
+ card.querySelector('[action="remove"]').click();
+ await removed;
+ is(card.parentNode, null, "Card has been removed from the view");
+ await TestUtils.waitForCondition(
+ () => enabledSection.querySelectorAll("addon-card").length == 1
+ );
+
+ let defaultTheme = getCardByAddonId(doc, "default-theme@mozilla.org");
+ is(defaultTheme.parentNode, enabledSection, "The default theme is reenabled");
+
+ await testUndoPendingUninstall(list, card.addon);
+ await TestUtils.waitForCondition(
+ () => enabledSection.querySelectorAll("addon-card").length == 1
+ );
+ is(defaultTheme.parentNode, disabledSection, "The default theme is disabled");
+ ok(getCardByAddonId(enabledSection, theme.id), "Theme should be reenabled");
+
+ await theme.unload();
+ await closeView(win);
+});
+
+add_task(async function testBuiltInThemeButtons() {
+ let win = await loadInitialView("theme");
+ let doc = win.document;
+
+ // Find the addon-list to listen for events.
+ let list = doc.querySelector("addon-list");
+ let enabledSection = getSection(doc, "theme-enabled-section");
+ let disabledSection = getSection(doc, "theme-disabled-section");
+
+ let defaultTheme = getCardByAddonId(doc, "default-theme@mozilla.org");
+ let darkTheme = getCardByAddonId(doc, "firefox-compact-dark@mozilla.org");
+
+ // Check that themes are in the expected spots.
+ is(defaultTheme.parentNode, enabledSection, "The default theme is enabled");
+ is(darkTheme.parentNode, disabledSection, "The dark theme is disabled");
+
+ // The default theme shouldn't have remove or disable options.
+ let defaultButtons = {
+ toggleDisabled: defaultTheme.querySelector('[action="toggle-disabled"]'),
+ remove: defaultTheme.querySelector('[action="remove"]'),
+ };
+ is(defaultButtons.toggleDisabled.hidden, true, "Disable is hidden");
+ is(defaultButtons.remove.hidden, true, "Remove is hidden");
+
+ // The dark theme should have an enable button, but not remove.
+ let darkButtons = {
+ toggleDisabled: darkTheme.querySelector('[action="toggle-disabled"]'),
+ remove: darkTheme.querySelector('[action="remove"]'),
+ };
+ is(darkButtons.toggleDisabled.hidden, false, "Enable is visible");
+ is(darkButtons.remove.hidden, true, "Remove is hidden");
+
+ // Enable the dark theme and check the buttons again.
+ let themesChanged = waitForThemeChange(list);
+ darkButtons.toggleDisabled.click();
+ await themesChanged;
+
+ await TestUtils.waitForCondition(
+ () => enabledSection.querySelectorAll("addon-card").length == 1
+ );
+
+ // Check the buttons.
+ is(defaultButtons.toggleDisabled.hidden, false, "Enable is visible");
+ is(defaultButtons.remove.hidden, true, "Remove is hidden");
+ is(darkButtons.toggleDisabled.hidden, false, "Disable is visible");
+ is(darkButtons.remove.hidden, true, "Remove is hidden");
+
+ // Disable the dark theme.
+ themesChanged = waitForThemeChange(list);
+ darkButtons.toggleDisabled.click();
+ await themesChanged;
+
+ await TestUtils.waitForCondition(
+ () => enabledSection.querySelectorAll("addon-card").length == 1
+ );
+
+ // The themes are back to their starting posititons.
+ is(defaultTheme.parentNode, enabledSection, "Default is enabled");
+ is(darkTheme.parentNode, disabledSection, "Dark is disabled");
+
+ await closeView(win);
+});
+
+add_task(async function testSideloadRemoveButton() {
+ const id = "sideload@mochi.test";
+ mockProvider.createAddons([
+ {
+ id,
+ name: "Sideloaded",
+ permissions: 0,
+ },
+ ]);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getCardByAddonId(doc, id);
+
+ let moreOptionsPanel = card.querySelector("panel-list");
+ let moreOptionsButton = card.querySelector('[action="more-options"]');
+ let panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown");
+ EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win);
+ await panelOpened;
+
+ // Verify the remove button is visible with a SUMO link.
+ let removeButton = card.querySelector('[action="remove"]');
+ ok(removeButton.disabled, "Remove is disabled");
+ ok(!removeButton.hidden, "Remove is visible");
+
+ // Remove but cancel.
+ let prevented = BrowserTestUtils.waitForEvent(card, "remove-disabled");
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a disabled control to confirm the click event
+ // won't come through. It is not meant to be interactive and is not expected
+ // to be accessible, therefore the rule check shall be ignored by a11y_checks.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ removeButton.click();
+ AccessibilityUtils.resetEnv();
+ await prevented;
+
+ // reopen the panel
+ panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown");
+ EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win);
+ await panelOpened;
+
+ let sumoLink = removeButton.querySelector("a");
+ ok(sumoLink, "There's a link");
+ is(
+ doc.l10n.getAttributes(removeButton).id,
+ "remove-addon-disabled-button",
+ "The can't remove text is shown"
+ );
+ sumoLink.focus();
+ is(doc.activeElement, sumoLink, "The link can be focused");
+
+ let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, REMOVE_SUMO_URL);
+ sumoLink.click();
+ BrowserTestUtils.removeTab(await newTabOpened);
+
+ await closeView(win);
+});
+
+add_task(async function testOnlyTypeIsShown() {
+ let win = await loadInitialView("theme");
+ let doc = win.document;
+
+ // Find the addon-list to listen for events.
+ let list = doc.querySelector("addon-list");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension",
+ browser_specific_settings: { gecko: { id: "test@mochi.test" } },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let skipped = BrowserTestUtils.waitForEvent(
+ list,
+ "skip-add",
+ e => e.detail == "type-mismatch"
+ );
+ await extension.startup();
+ await skipped;
+
+ let cards = getTestCards(list);
+ is(cards.length, 0, "There are no test extension cards");
+
+ await extension.unload();
+ await closeView(win);
+});
+
+add_task(async function testPluginIcons() {
+ const pluginIconUrl = "chrome://global/skin/icons/plugin.svg";
+
+ let win = await loadInitialView("plugin");
+ let doc = win.document;
+
+ // Check that the icons are set to the plugin icon.
+ let icons = doc.querySelectorAll(".card-heading-icon");
+ ok(!!icons.length, "There are some plugins listed");
+
+ for (let icon of icons) {
+ is(icon.src, pluginIconUrl, "Plugins use the plugin icon");
+ }
+
+ await closeView(win);
+});
+
+add_task(async function testExtensionGenericIcon() {
+ const extensionIconUrl =
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test extension",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getCardByAddonId(doc, id);
+ let icon = card.querySelector(".addon-icon");
+ is(icon.src, extensionIconUrl, "Extensions without icon use the generic one");
+
+ await extension.unload();
+ await closeView(win);
+});
+
+add_task(async function testSectionHeadingKeys() {
+ mockProvider.createAddons([
+ {
+ id: "test-theme",
+ name: "Test Theme",
+ type: "theme",
+ },
+ {
+ id: "test-extension-disabled",
+ name: "Test Disabled Extension",
+ type: "extension",
+ userDisabled: true,
+ },
+ {
+ id: "test-plugin-disabled",
+ name: "Test Disabled Plugin",
+ type: "plugin",
+ userDisabled: true,
+ },
+ {
+ id: "test-locale",
+ name: "Test Enabled Locale",
+ type: "locale",
+ },
+ {
+ id: "test-locale-disabled",
+ name: "Test Disabled Locale",
+ type: "locale",
+ userDisabled: true,
+ },
+ {
+ id: "test-dictionary",
+ name: "Test Enabled Dictionary",
+ type: "dictionary",
+ },
+ {
+ id: "test-dictionary-disabled",
+ name: "Test Disabled Dictionary",
+ type: "dictionary",
+ userDisabled: true,
+ },
+ {
+ id: "test-sitepermission",
+ name: "Test Enabled Site Permission",
+ type: "sitepermission",
+ },
+ {
+ id: "test-sitepermission-disabled",
+ name: "Test Disabled Site Permission",
+ type: "sitepermission",
+ userDisabled: true,
+ },
+ ]);
+
+ for (let type of [
+ "extension",
+ "theme",
+ "plugin",
+ "locale",
+ "dictionary",
+ "sitepermission",
+ ]) {
+ info(`loading view for addon type ${type}`);
+ let win = await loadInitialView(type);
+ let doc = win.document;
+
+ for (let status of ["enabled", "disabled"]) {
+ let section = getSection(doc, `${type}-${status}-section`);
+ let el = section?.querySelector(".list-section-heading");
+ isnot(el, null, `Should have ${status} heading for ${type} section`);
+ is(
+ el && doc.l10n.getAttributes(el).id,
+ win.getL10nIdMapping(`${type}-${status}-heading`),
+ `Should have correct ${status} heading for ${type} section`
+ );
+ }
+
+ await closeView(win);
+ }
+});
+
+add_task(async function testDisabledDimming() {
+ const id = "disabled@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Disable me",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let addon = await AddonManager.getAddonByID(id);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let pageHeader = doc.querySelector("addon-page-header");
+
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to clear the focused
+ // state with a mouse which can be done by assistive technology and keyboard
+ // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ // Ensure there's no focus on the list.
+ EventUtils.synthesizeMouseAtCenter(pageHeader, {}, win);
+ AccessibilityUtils.resetEnv();
+
+ const checkOpacity = (card, expected, msg) => {
+ let { opacity } = card.ownerGlobal.getComputedStyle(card.firstElementChild);
+ let normalize = val => Math.floor(val * 10);
+ is(normalize(opacity), normalize(expected), msg);
+ };
+ const waitForTransition = card =>
+ BrowserTestUtils.waitForEvent(
+ card.firstElementChild,
+ "transitionend",
+ /* capture = */ false,
+ e => e.propertyName === "opacity" && e.target.classList.contains("card")
+ );
+
+ let card = getCardByAddonId(doc, id);
+ checkOpacity(card, "1", "The opacity is 1 when enabled");
+
+ // Disable the add-on, check again.
+ let list = doc.querySelector("addon-list");
+ let moved = BrowserTestUtils.waitForEvent(list, "move");
+ await addon.disable();
+ await moved;
+
+ let disabledSection = getSection(doc, "extension-disabled-section");
+ is(card.parentNode, disabledSection, "The card is in the disabled section");
+ checkOpacity(card, "0.6", "The opacity is dimmed when disabled");
+
+ // Click on the menu button, this should un-dim the card.
+ let transitionEnded = waitForTransition(card);
+ let moreOptionsButton = card.querySelector(".more-options-button");
+ EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win);
+ await transitionEnded;
+ checkOpacity(card, "1", "The opacity is 1 when the menu is open");
+
+ // Close the menu, opacity should return.
+ transitionEnded = waitForTransition(card);
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to dismiss the opened
+ // menu with a mouse which can be done by assistive technology and keyboard
+ // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ EventUtils.synthesizeMouseAtCenter(pageHeader, {}, win);
+ AccessibilityUtils.resetEnv();
+ await transitionEnded;
+ checkOpacity(card, "0.6", "The card is dimmed again");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testEmptyMessage() {
+ let tests = [
+ {
+ type: "extension",
+ message: "Get extensions and themes on ",
+ },
+ {
+ type: "theme",
+ message: "Get extensions and themes on ",
+ },
+ {
+ type: "plugin",
+ message: "Get extensions and themes on ",
+ },
+ {
+ type: "locale",
+ message: "Get language packs on ",
+ },
+ {
+ type: "dictionary",
+ message: "Get dictionaries on ",
+ },
+ ];
+
+ for (let test of tests) {
+ let win = await loadInitialView(test.type);
+ let doc = win.document;
+ let enabledSection = getSection(doc, `${test.type}-enabled-section`);
+ let disabledSection = getSection(doc, `${test.type}-disabled-section`);
+ const message = doc.querySelector("#empty-addons-message");
+
+ // Test if the correct locale has been applied.
+ ok(
+ message.textContent.startsWith(test.message),
+ `View ${test.type} has correct empty list message`
+ );
+
+ // With at least one enabled/disabled add-on (see testSectionHeadingKeys),
+ // the message is hidden.
+ is_element_hidden(message, "Empty addons message hidden");
+
+ // The test runner (Mochitest) relies on add-ons that should not be removed.
+ // Simulate the scenario of zero add-ons by clearing all rendered sections.
+ while (enabledSection.firstChild) {
+ enabledSection.firstChild.remove();
+ }
+
+ while (disabledSection.firstChild) {
+ disabledSection.firstChild.remove();
+ }
+
+ // Message should now be displayed
+ is_element_visible(message, "Empty addons message visible");
+
+ await closeView(win);
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js
new file mode 100644
index 0000000000..db4067ab35
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+function makeResult({ guid, type }) {
+ return {
+ addon: {
+ authors: [{ name: "Some author" }],
+ current_version: {
+ files: [{ platform: "all", url: "data:," }],
+ },
+ url: "data:,",
+ guid,
+ type,
+ },
+ };
+}
+
+function mockResults() {
+ let types = ["extension", "theme", "extension", "extension", "theme"];
+ return {
+ results: types.map((type, i) =>
+ makeResult({
+ guid: `${type}${i}@mochi.test`,
+ type,
+ })
+ ),
+ };
+}
+
+add_setup(async function () {
+ let results = btoa(JSON.stringify(mockResults()));
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable personalized recommendations, they will break the data URI.
+ ["browser.discovery.enabled", false],
+ ["extensions.getAddons.discovery.api_url", `data:;base64,${results}`],
+ [
+ "extensions.recommendations.themeRecommendationUrl",
+ "https://example.com/theme",
+ ],
+ ],
+ });
+});
+
+function checkExtraContents(doc, type, opts = {}) {
+ let { showThemeRecommendationFooter = type === "theme" } = opts;
+ let footer = doc.querySelector("footer");
+ let amoButton = footer.querySelector('[action="open-amo"]');
+ let privacyPolicyLink = footer.querySelector(".privacy-policy-link");
+ let themeRecommendationFooter = footer.querySelector(".theme-recommendation");
+ let themeRecommendationLink =
+ themeRecommendationFooter && themeRecommendationFooter.querySelector("a");
+ let taarNotice = doc.querySelector("taar-notice");
+
+ is_element_visible(footer, "The footer is visible");
+
+ if (type == "extension") {
+ ok(taarNotice, "There is a TAAR notice");
+ is_element_visible(amoButton, "The AMO button is shown");
+ is_element_visible(privacyPolicyLink, "The privacy policy is visible");
+ } else if (type == "theme") {
+ ok(!taarNotice, "There is no TAAR notice");
+ ok(amoButton, "AMO button is shown");
+ ok(!privacyPolicyLink, "There is no privacy policy");
+ } else {
+ throw new Error(`Unknown type ${type}`);
+ }
+
+ if (showThemeRecommendationFooter) {
+ is_element_visible(
+ themeRecommendationFooter,
+ "There's a theme recommendation footer"
+ );
+ is_element_visible(themeRecommendationLink, "There's a link to the theme");
+ is(themeRecommendationLink.target, "_blank", "The link opens in a new tab");
+ is(
+ themeRecommendationLink.href,
+ "https://example.com/theme",
+ "The link goes to the pref's URL"
+ );
+ is(
+ doc.l10n.getAttributes(themeRecommendationFooter).id,
+ "recommended-theme-1",
+ "The recommendation has the right l10n-id"
+ );
+ } else {
+ ok(
+ !themeRecommendationFooter || themeRecommendationFooter.hidden,
+ "There's no theme recommendation"
+ );
+ }
+}
+
+async function installAddon({ card, recommendedList, manifestExtra = {} }) {
+ // Install an add-on to hide the card.
+ let hidden = BrowserTestUtils.waitForEvent(
+ recommendedList,
+ "card-hidden",
+ false,
+ e => e.detail.card == card
+ );
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: card.addonId } },
+ ...manifestExtra,
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ await hidden;
+ return extension;
+}
+
+async function testListRecommendations({ type, manifestExtra = {} }) {
+ let win = await loadInitialView(type);
+ let doc = win.document;
+
+ // Wait for the list to render, rendering is tested with the discovery pane.
+ let recommendedList = doc.querySelector("recommended-addon-list");
+ await recommendedList.cardsReady;
+
+ checkExtraContents(doc, type);
+
+ // Check that the cards are all for the right type.
+ let cards = doc.querySelectorAll("recommended-addon-card");
+ ok(!!cards.length, "There were some cards found");
+ for (let card of cards) {
+ is(card.discoAddon.type, type, `The card is for a ${type}`);
+ is_element_visible(card, "The card is visible");
+ }
+
+ // Install an add-on for the first card, verify it is hidden.
+ let { addonId } = cards[0];
+ ok(addonId, "The card has an addonId");
+
+ // Installing the add-on will fail since the URL doesn't point to a valid
+ // XPI.
+ let installButton = cards[0].querySelector('[action="install-addon"]');
+ let { panel } = PopupNotifications;
+ let popupId = "addon-install-failed-notification";
+ let failPromise = TestUtils.topicObserved("addon-install-failed");
+ installButton.click();
+ await failPromise;
+ // Wait for the installing popup to be hidden and leave just the error popup.
+ await BrowserTestUtils.waitForCondition(() => {
+ return panel.children.length == 1 && panel.firstElementChild.id == popupId;
+ });
+
+ // Dismiss the popup.
+ panel.firstElementChild.button.click();
+ await BrowserTestUtils.waitForPopupEvent(panel, "hidden");
+
+ let extension = await installAddon({ card: cards[0], recommendedList });
+ is_element_hidden(cards[0], "The card is now hidden");
+
+ // Switch away and back, there should still be a hidden card.
+ await closeView(win);
+ win = await loadInitialView(type);
+ doc = win.document;
+ recommendedList = doc.querySelector("recommended-addon-list");
+ await recommendedList.cardsReady;
+
+ cards = Array.from(doc.querySelectorAll("recommended-addon-card"));
+
+ let hiddenCard = cards.pop();
+ is(hiddenCard.addonId, addonId, "The expected card was found");
+ is_element_hidden(hiddenCard, "The card is still hidden");
+
+ ok(!!cards.length, "There are still some visible cards");
+ for (let card of cards) {
+ is(card.discoAddon.type, type, `The card is for a ${type}`);
+ is_element_visible(card, "The card is visible");
+ }
+
+ // Uninstall the add-on, verify the card is shown again.
+ let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown");
+ await extension.unload();
+ await shown;
+
+ is_element_visible(hiddenCard, "The card is now shown");
+
+ await closeView(win);
+}
+
+add_task(async function testExtensionList() {
+ await testListRecommendations({ type: "extension" });
+});
+
+add_task(async function testThemeList() {
+ await testListRecommendations({
+ type: "theme",
+ manifestExtra: { theme: {} },
+ });
+});
+
+add_task(async function testInstallAllExtensions() {
+ let type = "extension";
+ let win = await loadInitialView(type);
+ let doc = win.document;
+
+ // Wait for the list to render, rendering is tested with the discovery pane.
+ let recommendedList = doc.querySelector("recommended-addon-list");
+ await recommendedList.cardsReady;
+
+ // Find more button is shown.
+ checkExtraContents(doc, type);
+
+ let cards = Array.from(doc.querySelectorAll("recommended-addon-card"));
+ is(cards.length, 3, "We found some cards");
+
+ let extensions = await Promise.all(
+ cards.map(card => installAddon({ card, recommendedList }))
+ );
+
+ // The find more on AMO button is shown.
+ checkExtraContents(doc, type);
+
+ // Uninstall one of the extensions, the button should still be shown.
+ let extension = extensions.pop();
+ let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown");
+ await extension.unload();
+ await shown;
+
+ // The find more on AMO button is shown.
+ checkExtraContents(doc, type);
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+ await closeView(win);
+});
+
+add_task(async function testError() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.getAddons.discovery.api_url", "data:,"]],
+ });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Wait for the list to render, rendering is tested with the discovery pane.
+ let recommendedList = doc.querySelector("recommended-addon-list");
+ await recommendedList.cardsReady;
+
+ checkExtraContents(doc, "extension");
+
+ await closeView(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testThemesNoRecommendationUrl() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.recommendations.themeRecommendationUrl", ""]],
+ });
+
+ let win = await loadInitialView("theme");
+ let doc = win.document;
+
+ // Wait for the list to render, rendering is tested with the discovery pane.
+ let recommendedList = doc.querySelector("recommended-addon-list");
+ await recommendedList.cardsReady;
+
+ checkExtraContents(doc, "theme", { showThemeRecommendationFooter: false });
+
+ await closeView(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testRecommendationsDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.htmlaboutaddons.recommendations.enabled", false]],
+ });
+
+ let types = ["extension", "theme"];
+
+ for (let type of types) {
+ let win = await loadInitialView(type);
+ let doc = win.document;
+
+ let recommendedList = doc.querySelector("recommended-addon-list");
+ ok(!recommendedList, `There are no recommendations on the ${type} page`);
+
+ await closeView(win);
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js b/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js
new file mode 100644
index 0000000000..b60baf8799
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint max-len: ["error", 80] */
+
+let htmlAboutAddonsWindow;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+function clickElement(el) {
+ el.dispatchEvent(new CustomEvent("click"));
+}
+
+function createMessageBar(messageBarStack, { attrs, children, onclose } = {}) {
+ const win = messageBarStack.ownerGlobal;
+ const messageBar = win.document.createElementNS(HTML_NS, "message-bar");
+ if (attrs) {
+ for (const [k, v] of Object.entries(attrs)) {
+ messageBar.setAttribute(k, v);
+ }
+ }
+ if (children) {
+ if (Array.isArray(children)) {
+ messageBar.append(...children);
+ } else {
+ messageBar.append(children);
+ }
+ }
+ messageBar.addEventListener("message-bar:close", onclose, { once: true });
+ messageBarStack.append(messageBar);
+ return messageBar;
+}
+
+add_setup(async function () {
+ htmlAboutAddonsWindow = await loadInitialView("extension");
+ registerCleanupFunction(() => closeView(htmlAboutAddonsWindow));
+});
+
+add_task(async function test_message_bar_stack() {
+ const win = htmlAboutAddonsWindow;
+
+ let messageBarStack = win.document.getElementById("abuse-reports-messages");
+
+ ok(messageBarStack, "Got a message-bar-stack in HTML about:addons page");
+
+ is(
+ messageBarStack.maxMessageBarCount,
+ 3,
+ "Got the expected max-message-bar-count property"
+ );
+
+ is(
+ messageBarStack.childElementCount,
+ 0,
+ "message-bar-stack is initially empty"
+ );
+});
+
+add_task(async function test_create_message_bar_create_and_onclose() {
+ const win = htmlAboutAddonsWindow;
+ const messageBarStack = win.document.getElementById("abuse-reports-messages");
+
+ let messageEl = win.document.createElementNS(HTML_NS, "span");
+ messageEl.textContent = "A message bar text";
+ let buttonEl = win.document.createElementNS(HTML_NS, "button");
+ buttonEl.textContent = "An action button";
+
+ let messageBar;
+ let onceMessageBarClosed = new Promise(resolve => {
+ messageBar = createMessageBar(messageBarStack, {
+ children: [messageEl, buttonEl],
+ onclose: resolve,
+ });
+ });
+
+ is(
+ messageBarStack.childElementCount,
+ 1,
+ "message-bar-stack has a child element"
+ );
+ is(
+ messageBarStack.firstElementChild,
+ messageBar,
+ "newly created message-bar added as message-bar-stack child element"
+ );
+
+ const slot = messageBar.shadowRoot.querySelector("slot");
+ is(
+ slot.assignedNodes()[0],
+ messageEl,
+ "Got the expected span element assigned to the message-bar slot"
+ );
+ is(
+ slot.assignedNodes()[1],
+ buttonEl,
+ "Got the expected button element assigned to the message-bar slot"
+ );
+
+ let dismissed = BrowserTestUtils.waitForEvent(
+ messageBar,
+ "message-bar:user-dismissed"
+ );
+ info("Click the close icon on the newly created message-bar");
+ clickElement(messageBar.closeButton);
+ await dismissed;
+
+ info("Expect the onclose function to be called");
+ await onceMessageBarClosed;
+
+ is(
+ messageBarStack.childElementCount,
+ 0,
+ "message-bar-stack has no child elements"
+ );
+});
+
+add_task(async function test_max_message_bar_count() {
+ const win = htmlAboutAddonsWindow;
+ const messageBarStack = win.document.getElementById("abuse-reports-messages");
+
+ info("Create a new message-bar");
+ let messageElement = document.createElementNS(HTML_NS, "span");
+ messageElement = "message bar label";
+
+ let onceMessageBarClosed = new Promise(resolve => {
+ createMessageBar(messageBarStack, {
+ children: messageElement,
+ onclose: resolve,
+ });
+ });
+
+ is(
+ messageBarStack.childElementCount,
+ 1,
+ "message-bar-stack has the expected number of children"
+ );
+
+ info("Create 3 more message bars");
+ const allBarsPromises = [];
+ for (let i = 2; i <= 4; i++) {
+ allBarsPromises.push(
+ new Promise(resolve => {
+ createMessageBar(messageBarStack, {
+ attrs: { dismissable: "" },
+ children: [messageElement, i],
+ onclose: resolve,
+ });
+ })
+ );
+ }
+
+ info("Expect first message-bar to closed automatically");
+ await onceMessageBarClosed;
+
+ is(
+ messageBarStack.childElementCount,
+ 3,
+ "message-bar-stack has the expected number of children"
+ );
+
+ info("Click on close icon for the second message-bar");
+ clickElement(messageBarStack.firstElementChild.closeButton);
+
+ info("Expect the second message-bar to be closed");
+ await allBarsPromises[0];
+
+ is(
+ messageBarStack.childElementCount,
+ 2,
+ "message-bar-stack has the expected number of children"
+ );
+
+ info("Clear the entire message-bar-stack content");
+ messageBarStack.textContent = "";
+
+ info("Expect all the created message-bar to be closed automatically");
+ await Promise.all(allBarsPromises);
+
+ is(
+ messageBarStack.childElementCount,
+ 0,
+ "message-bar-stack has no child elements"
+ );
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js
new file mode 100644
index 0000000000..c5bfa1022f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js
@@ -0,0 +1,651 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+// This test function helps to detect when an addon options browser have been
+// inserted in the about:addons page.
+function waitOptionsBrowserInserted() {
+ return new Promise(resolve => {
+ async function listener(eventName, browser) {
+ // wait for a webextension XUL browser element that is owned by the
+ // "about:addons" page.
+ if (browser.ownerGlobal.top.location.href == "about:addons") {
+ ExtensionParent.apiManager.off("extension-browser-inserted", listener);
+ resolve(browser);
+ }
+ }
+ ExtensionParent.apiManager.on("extension-browser-inserted", listener);
+ });
+}
+
+add_task(async function enableHtmlViews() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.htmlaboutaddons.inline-options.enabled", true]],
+ });
+});
+
+add_task(async function testInlineOptions() {
+ const HEIGHT_SHORT = 300;
+ const HEIGHT_TALL = 600;
+
+ let id = "inline@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+ <html>
+ <head>
+ <style type="text/css">
+ body > p { height: ${HEIGHT_SHORT}px; margin: 0; }
+ body.bigger > p { height: ${HEIGHT_TALL}px; }
+ </style>
+ <script src="options.js"></script>
+ </head>
+ <body>
+ <p>Some text</p>
+ </body>
+ </html>
+ `,
+ "options.js": () => {
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "toggle-class") {
+ document.body.classList.toggle("bigger");
+ } else if (msg == "get-height") {
+ browser.test.sendMessage("height", document.body.clientHeight);
+ }
+ });
+
+ browser.test.sendMessage("options-loaded", window.location.href);
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Make sure we found the right card.
+ let card = getAddonCard(win, id);
+ ok(card, "Found the card");
+
+ // The preferences option should be visible.
+ let preferences = card.querySelector('[action="preferences"]');
+ ok(!preferences.hidden, "The preferences option is visible");
+
+ // Open the preferences page.
+ let loaded = waitForViewLoad(win);
+ preferences.click();
+ await loaded;
+
+ // Verify we're on the preferences tab.
+ card = doc.querySelector("addon-card");
+ is(card.addon.id, id, "The right page was loaded");
+ let { deck, tabGroup } = card.details;
+ let { selectedViewName } = deck;
+ is(selectedViewName, "preferences", "The preferences tab is shown");
+
+ info("Check that there are two buttons and they're visible");
+ let detailsBtn = tabGroup.querySelector('[name="details"]');
+ ok(!detailsBtn.hidden, "The details button is visible");
+ let prefsBtn = tabGroup.querySelector('[name="preferences"]');
+ ok(!prefsBtn.hidden, "The preferences button is visible");
+
+ // Wait for the browser to load.
+ let url = await extension.awaitMessage("options-loaded");
+
+ // Check the attributes of the options browser.
+ let browser = card.querySelector("inline-options-browser browser");
+ ok(browser, "The visible view has a browser");
+ is(
+ browser.currentURI.spec,
+ card.addon.optionsURL,
+ "The browser has the expected options URL"
+ );
+ is(url, card.addon.optionsURL, "Browser has the expected options URL loaded");
+ let stack = browser.closest("stack");
+ is(
+ browser.clientWidth,
+ stack.clientWidth,
+ "Browser should be the same width as its direct parent"
+ );
+ Assert.greater(stack.clientWidth, 0, "The stack has a width");
+ ok(
+ card.querySelector('[action="preferences"]').hidden,
+ "The preferences option is hidden now"
+ );
+
+ let waitForHeightChange = expectedHeight =>
+ TestUtils.waitForCondition(() => browser.clientHeight === expectedHeight);
+
+ await waitForHeightChange(HEIGHT_SHORT);
+
+ // Check resizing the browser through extension CSS.
+ await extension.sendMessage("get-height");
+ let height = await extension.awaitMessage("height");
+ is(height, HEIGHT_SHORT, "The height is smaller to start");
+ is(height, browser.clientHeight, "The browser is the same size");
+
+ info("Resize the browser to be taller");
+ await extension.sendMessage("toggle-class");
+ await waitForHeightChange(HEIGHT_TALL);
+ await extension.sendMessage("get-height");
+ height = await extension.awaitMessage("height");
+ is(height, HEIGHT_TALL, "The height is bigger now");
+ is(height, browser.clientHeight, "The browser is the same size");
+
+ info("Shrink the browser again");
+ await extension.sendMessage("toggle-class");
+ await waitForHeightChange(HEIGHT_SHORT);
+ await extension.sendMessage("get-height");
+ height = await extension.awaitMessage("height");
+ is(height, HEIGHT_SHORT, "The browser shrunk back");
+ is(height, browser.clientHeight, "The browser is the same size");
+
+ info("Switching to details view");
+ detailsBtn.click();
+
+ info("Check the browser dimensions to make sure it's hidden");
+ is(browser.clientWidth, 0, "The browser is hidden now");
+
+ info("Switch back, check browser is shown");
+ prefsBtn.click();
+
+ is(browser.clientWidth, stack.clientWidth, "The browser width is set again");
+ Assert.greater(stack.clientWidth, 0, "The stack has a width");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+// Regression test against bug 1409697
+add_task(async function testCardRerender() {
+ let id = "rerender@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+ <html>
+ <body>
+ <p>Some text</p>
+ </body>
+ </html>
+ `,
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(win, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ await browserAdded;
+
+ is(
+ doc.querySelectorAll("inline-options-browser").length,
+ 1,
+ "There is 1 inline-options-browser"
+ );
+ is(doc.querySelectorAll("browser").length, 1, "There is 1 browser");
+
+ info("Reload the add-on and ensure there's still only one browser");
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ card.addon.reload();
+ await updated;
+
+ // Since the add-on was disabled, we'll be on the details tab.
+ is(card.details.deck.selectedViewName, "details", "View changed to details");
+ is(
+ doc.querySelectorAll("inline-options-browser").length,
+ 1,
+ "There is 1 inline-options-browser"
+ );
+ is(doc.querySelectorAll("browser").length, 0, "The browser was destroyed");
+
+ // Load the permissions tab again.
+ browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ await browserAdded;
+
+ // Switching to preferences will create a new browser element.
+ is(
+ card.details.deck.selectedViewName,
+ "preferences",
+ "View switched to preferences"
+ );
+ is(
+ doc.querySelectorAll("inline-options-browser").length,
+ 1,
+ "There is 1 inline-options-browser"
+ );
+ is(doc.querySelectorAll("browser").length, 1, "There is a new browser");
+
+ info("Re-rendering card to ensure a second browser isn't added");
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ card.render();
+ await updated;
+
+ is(
+ card.details.deck.selectedViewName,
+ "details",
+ "Rendering reverted to the details view"
+ );
+ is(
+ doc.querySelectorAll("inline-options-browser").length,
+ 1,
+ "There is still only 1 inline-options-browser after re-render"
+ );
+ is(doc.querySelectorAll("browser").length, 0, "There is no browser");
+
+ let newBrowserAdded = waitOptionsBrowserInserted();
+ card.showPrefs();
+ await newBrowserAdded;
+
+ is(
+ doc.querySelectorAll("inline-options-browser").length,
+ 1,
+ "There is still only 1 inline-options-browser after opening preferences"
+ );
+ is(doc.querySelectorAll("browser").length, 1, "There is 1 browser");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testRemovedOnDisable() {
+ let id = "disable@mochi.test";
+ const xpiFile = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": "<h1>Options!</h1>",
+ },
+ });
+ let addon = await AddonManager.installTemporaryAddon(xpiFile);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Opens the prefs page.
+ let loaded = waitForViewLoad(win);
+ getAddonCard(win, id).querySelector("[action=preferences]").click();
+ await loaded;
+
+ let inlineOptions = doc.querySelector("inline-options-browser");
+ ok(inlineOptions, "There's an inline-options-browser element");
+ ok(inlineOptions.querySelector("browser"), "The browser exists");
+
+ let card = getAddonCard(win, id);
+ let { deck } = card.details;
+ is(deck.selectedViewName, "preferences", "Preferences are the active tab");
+
+ info("Disabling the add-on");
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ await addon.disable();
+ await updated;
+
+ is(deck.selectedViewName, "details", "Details are now the active tab");
+ ok(inlineOptions, "There's an inline-options-browser element");
+ ok(!inlineOptions.querySelector("browser"), "The browser has been removed");
+
+ info("Enabling the add-on");
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ await addon.enable();
+ await updated;
+
+ is(deck.selectedViewName, "details", "Details are still the active tab");
+ ok(inlineOptions, "There's an inline-options-browser element");
+ ok(!inlineOptions.querySelector("browser"), "The browser is not created yet");
+
+ info("Switching to preferences tab");
+ let changed = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ let browserAdded = waitOptionsBrowserInserted();
+ deck.selectedViewName = "preferences";
+ await changed;
+ await browserAdded;
+
+ is(deck.selectedViewName, "preferences", "Preferences are selected");
+ ok(inlineOptions, "There's an inline-options-browser element");
+ ok(inlineOptions.querySelector("browser"), "The browser is re-created");
+
+ await closeView(win);
+ await addon.uninstall();
+});
+
+add_task(async function testUpgradeTemporary() {
+ let id = "upgrade-temporary@mochi.test";
+ async function loadExtension(version) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ version,
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+ <html>
+ <head>
+ <script src="options.js"></script>
+ </head>
+ <body>
+ <p>Version <pre>${version}</pre></p>
+ </body>
+ </html>
+ `,
+ "options.js": () => {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "get-version") {
+ let version = document.querySelector("pre").textContent;
+ browser.test.sendMessage("version", version);
+ }
+ });
+ window.onload = () => browser.test.sendMessage("options-loaded");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ return extension;
+ }
+
+ let firstExtension = await loadExtension("1");
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(win, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ await browserAdded;
+
+ await firstExtension.awaitMessage("options-loaded");
+ await firstExtension.sendMessage("get-version");
+ let version = await firstExtension.awaitMessage("version");
+ is(version, "1", "Version 1 page is loaded");
+
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ browserAdded = waitOptionsBrowserInserted();
+ let secondExtension = await loadExtension("2");
+ await updated;
+ await browserAdded;
+ await secondExtension.awaitMessage("options-loaded");
+
+ await secondExtension.sendMessage("get-version");
+ version = await secondExtension.awaitMessage("version");
+ is(version, "2", "Version 2 page is loaded");
+ let { deck } = card.details;
+ is(deck.selectedViewName, "preferences", "Preferences are still shown");
+
+ await closeView(win);
+ await firstExtension.unload();
+ await secondExtension.unload();
+});
+
+add_task(async function testReloadExtension() {
+ let id = "reload@mochi.test";
+ let xpiFile = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+ <html>
+ <head>
+ </head>
+ <body>
+ <p>Options</p>
+ </body>
+ </html>
+ `,
+ },
+ });
+ let addon = await AddonManager.installTemporaryAddon(xpiFile);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(win, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+ let { deck } = card.details;
+ is(deck.selectedViewName, "details", "Details load first");
+
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ await browserAdded;
+
+ is(deck.selectedViewName, "preferences", "Preferences are shown");
+
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ browserAdded = waitOptionsBrowserInserted();
+ let addonStarted = AddonTestUtils.promiseWebExtensionStartup(id);
+ await addon.reload();
+ await addonStarted;
+ await updated;
+ await browserAdded;
+ is(deck.selectedViewName, "preferences", "Preferences are still shown");
+
+ await closeView(win);
+ await addon.uninstall();
+});
+
+async function testSelectPosition(optionsBrowser, zoom) {
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ await BrowserTestUtils.synthesizeMouseAtCenter("select", {}, optionsBrowser);
+ let popup = await popupShownPromise;
+ let popupLeft = popup.shadowRoot.querySelector(
+ ".menupopup-arrowscrollbox"
+ ).screenX;
+ let browserLeft = optionsBrowser.screenX * zoom;
+ Assert.lessOrEqual(
+ Math.abs(popupLeft - browserLeft),
+ 1,
+ `Popup should be correctly positioned: ${popupLeft} vs. ${browserLeft}`
+ );
+ popup.hidePopup();
+}
+
+async function testOptionsZoom(type = "full") {
+ let id = `${type}-zoom@mochi.test`;
+ let zoomProp = `${type}Zoom`;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+ <!doctype html>
+ <script src="options.js"></script>
+ <body style="height: 500px">
+ <p>Some text</p>
+ <p>
+ <select>
+ <option>A</option>
+ <option>B</option>
+ </select>
+ </p>
+ </body>
+ `,
+ "options.js": () => {
+ window.addEventListener("load", function () {
+ browser.test.sendMessage("options-loaded");
+ });
+ },
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ gBrowser.selectedBrowser[zoomProp] = 2;
+
+ let card = getAddonCard(win, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ let optionsBrowser = await browserAdded;
+ // Wait for the browser to load.
+ await extension.awaitMessage("options-loaded");
+
+ is(optionsBrowser[zoomProp], 2, `Options browser inherited ${zoomProp}`);
+
+ await testSelectPosition(optionsBrowser, type == "full" ? 2 : 1);
+
+ gBrowser.selectedBrowser[zoomProp] = 0.5;
+
+ is(
+ optionsBrowser[zoomProp],
+ 0.5,
+ `Options browser reacts to ${zoomProp} change`
+ );
+
+ await closeView(win);
+ await extension.unload();
+}
+
+add_task(function testOptionsFullZoom() {
+ return testOptionsZoom("full");
+});
+
+add_task(function testOptionsTextZoom() {
+ return testOptionsZoom("text");
+});
+
+add_task(async function testInputAndQuickFind() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+ <html>
+ <body>
+ <input name="some-input" type="text">
+ <script src="options.js"></script>
+ </body>
+ </html>
+ `,
+ "options.js": () => {
+ let input = document.querySelector("input");
+ browser.test.assertEq(
+ "some-input",
+ input.getAttribute("name"),
+ "Expected options page input"
+ );
+ input.addEventListener("input", event => {
+ browser.test.sendMessage("input-changed", event.target.value);
+ });
+
+ browser.test.sendMessage("options-loaded", window.location.href);
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ // Make sure we found the right card.
+ let card = getAddonCard(win, extension.id);
+ ok(card, "Found the card");
+
+ // The preferences option should be visible.
+ let preferences = card.querySelector('[action="preferences"]');
+ ok(!preferences.hidden, "The preferences option is visible");
+
+ // Open the preferences page.
+ let loaded = waitForViewLoad(win);
+ preferences.click();
+ await loaded;
+
+ // Verify we're on the preferences tab.
+ card = doc.querySelector("addon-card");
+ is(card.addon.id, extension.id, "The right page was loaded");
+
+ // Wait for the browser to load.
+ let url = await extension.awaitMessage("options-loaded");
+
+ // Check the attributes of the options browser.
+ let browser = card.querySelector("inline-options-browser browser");
+ ok(browser, "The visible view has a browser");
+ ok(card.addon.optionsURL.length, "Options URL is not empty");
+ is(
+ browser.currentURI.spec,
+ card.addon.optionsURL,
+ "The browser has the expected options URL"
+ );
+ is(url, card.addon.optionsURL, "Browser has the expected options URL loaded");
+
+ // Focus the options browser.
+ browser.focus();
+
+ // Focus the input in the options page.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("input").focus();
+ });
+
+ info("input in options page should be focused, typing...");
+ // Type '/'.
+ EventUtils.synthesizeKey("/");
+
+ let inputValue = await extension.awaitMessage("input-changed");
+ is(inputValue, "/", "Expected input to contain a slash");
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js
new file mode 100644
index 0000000000..68faecfec0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+add_task(async function enableHtmlViews() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.htmlaboutaddons.inline-options.enabled", true]],
+ });
+});
+
+async function testOptionsInTab({ id, options_ui_options }) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Prefs extension",
+ browser_specific_settings: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ ...options_ui_options,
+ },
+ },
+ background() {
+ browser.test.sendMessage(
+ "options-url",
+ browser.runtime.getURL("options.html")
+ );
+ },
+ files: {
+ "options.html": `<script src="options.js"></script>`,
+ "options.js": () => {
+ browser.test.sendMessage("options-loaded");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let optionsUrl = await extension.awaitMessage("options-url");
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let aboutAddonsTab = gBrowser.selectedTab;
+
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ let prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(!prefsBtn.hidden, "The button is not hidden");
+
+ info("Open the preferences page from list");
+ let tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, optionsUrl);
+ prefsBtn.click();
+ await extension.awaitMessage("options-loaded");
+ BrowserTestUtils.removeTab(await tabLoaded);
+
+ info("Load details page");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Find the expanded card.
+ card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ info("Check that the button is still visible");
+ prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(!prefsBtn.hidden, "The button is not hidden");
+
+ info("Open the preferences page from details");
+ tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, optionsUrl);
+ prefsBtn.click();
+ let prefsTab = await tabLoaded;
+ await extension.awaitMessage("options-loaded");
+
+ info("Switch back to about:addons and open prefs again");
+ await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab);
+ let tabSwitched = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone");
+ prefsBtn.click();
+ await tabSwitched;
+ is(gBrowser.selectedTab, prefsTab, "The prefs tab was selected");
+
+ BrowserTestUtils.removeTab(prefsTab);
+
+ await closeView(win);
+ await extension.unload();
+}
+
+add_task(async function testPreferencesLink() {
+ let id = "prefs@mochi.test";
+ await testOptionsInTab({ id, options_ui_options: { open_in_tab: true } });
+});
+
+add_task(async function testPreferencesInlineDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.htmlaboutaddons.inline-options.enabled", false]],
+ });
+
+ let id = "inline-disabled@mochi.test";
+ await testOptionsInTab({ id, options_ui_options: {} });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testNoPreferences() {
+ let id = "no-prefs@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "No Prefs extension",
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ info("Check button on list");
+ let prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(prefsBtn.hidden, "The button is hidden");
+
+ info("Load details page");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Find the expanded card.
+ card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ info("Check that the button is still hidden on detail");
+ prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(prefsBtn.hidden, "The button is hidden");
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js
new file mode 100644
index 0000000000..f3616cd080
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js
@@ -0,0 +1,311 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const server = AddonTestUtils.createHttpServer();
+
+const LOCALE_ADDON_ID = "postponed-langpack@mochi.test";
+
+let gProvider;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.checkUpdateSecurity", false]],
+ });
+
+ // Also include a langpack with a pending postponed install.
+ const fakeLocalePostponedInstall = {
+ name: "updated langpack",
+ version: "2.0",
+ state: AddonManager.STATE_POSTPONED,
+ };
+
+ gProvider = new MockProvider();
+ gProvider.createAddons([
+ {
+ id: LOCALE_ADDON_ID,
+ name: "Postponed Langpack",
+ type: "locale",
+ version: "1.0",
+ // Mock pending upgrade property on the mocked langpack add-on.
+ pendingUpgrade: {
+ install: fakeLocalePostponedInstall,
+ },
+ },
+ ]);
+
+ fakeLocalePostponedInstall.existingAddon = gProvider.addons[0];
+ gProvider.createInstalls([fakeLocalePostponedInstall]);
+
+ registerCleanupFunction(() => {
+ cleanupPendingNotifications();
+ });
+});
+
+function createTestExtension({
+ id = "test-pending-update@test",
+ newManifest = {},
+}) {
+ function background() {
+ browser.runtime.onUpdateAvailable.addListener(() => {
+ browser.test.sendMessage("update-available");
+ });
+
+ browser.test.sendMessage("bgpage-ready");
+ }
+
+ const serverHost = `http://localhost:${server.identity.primaryPort}`;
+ const updatesPath = `/ext-updates-${id}.json`;
+ const update_url = `${serverHost}${updatesPath}`;
+
+ const manifest = {
+ name: "Test Pending Update",
+ browser_specific_settings: {
+ gecko: { id, update_url },
+ },
+ version: "1",
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest,
+ // Use permanent so the add-on can be updated.
+ useAddonManager: "permanent",
+ });
+
+ let updateXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ ...manifest,
+ ...newManifest,
+ version: "2",
+ },
+ });
+
+ let xpiFilename = `/update-${id}.xpi`;
+ server.registerFile(xpiFilename, updateXpi);
+ AddonTestUtils.registerJSON(server, updatesPath, {
+ addons: {
+ [id]: {
+ updates: [
+ {
+ version: "2",
+ update_link: serverHost + xpiFilename,
+ },
+ ],
+ },
+ },
+ });
+
+ return { extension, updateXpi };
+}
+
+async function promiseUpdateAvailable(extension) {
+ info("Wait for the extension to receive onUpdateAvailable event");
+ await extension.awaitMessage("update-available");
+}
+
+function expectUpdatesAvailableBadgeCount({ win, expectedNumber }) {
+ const categoriesSidebar = win.document.querySelector("categories-box");
+ ok(categoriesSidebar, "Found the categories-box element");
+ const availableButton =
+ categoriesSidebar.getButtonByName("available-updates");
+ is(
+ availableButton.badgeCount,
+ 1,
+ `Expect only ${expectedNumber} available updates`
+ );
+ ok(
+ !availableButton.hidden,
+ "Expecte the available updates category to be visible"
+ );
+}
+
+async function expectAddonInstallStatePostponed(id) {
+ const [addonInstall] = (await AddonManager.getAllInstalls()).filter(
+ install => install.existingAddon && install.existingAddon.id == id
+ );
+ is(
+ addonInstall && addonInstall.state,
+ AddonManager.STATE_POSTPONED,
+ "AddonInstall is in the postponed state"
+ );
+}
+
+function expectCardOptionsButtonBadged({ id, win, hasBadge = true }) {
+ const card = getAddonCard(win, id);
+ const moreOptionsEl = card.querySelector(".more-options-button");
+ is(
+ moreOptionsEl.classList.contains("more-options-button-badged"),
+ hasBadge,
+ `The options button should${hasBadge || "n't"} have the update badge`
+ );
+}
+
+function getCardPostponedBar({ id, win }) {
+ const card = getAddonCard(win, id);
+ return card.querySelector(".update-postponed-bar");
+}
+
+function waitCardAndAddonUpdated({ id, win }) {
+ const card = getAddonCard(win, id);
+ const updatedExtStarted = AddonTestUtils.promiseWebExtensionStartup(id);
+ const updatedCard = BrowserTestUtils.waitForEvent(card, "update");
+ return Promise.all([updatedExtStarted, updatedCard]);
+}
+
+async function testPostponedBarVisibility({ id, win, hidden = false }) {
+ const postponedBar = getCardPostponedBar({ id, win });
+ is(
+ postponedBar.hidden,
+ hidden,
+ `${id} update postponed message bar should be ${
+ hidden ? "hidden" : "visible"
+ }`
+ );
+
+ if (!hidden) {
+ await expectAddonInstallStatePostponed(id);
+ }
+}
+
+async function assertPostponedBarVisibleInAllViews({ id, win }) {
+ info("Test postponed bar visibility in extension list view");
+ await testPostponedBarVisibility({ id, win });
+
+ info("Test postponed bar visibility in available view");
+ await switchView(win, "available-updates");
+ await testPostponedBarVisibility({ id, win });
+
+ info("Test that available updates count do not include postponed langpacks");
+ expectUpdatesAvailableBadgeCount({ win, expectedNumber: 1 });
+
+ info("Test postponed langpacks are not listed in the available updates view");
+ ok(
+ !getAddonCard(win, LOCALE_ADDON_ID),
+ "Locale addon is expected to not be listed in the updates view"
+ );
+
+ info("Test that postponed bar isn't visible on postponed langpacks");
+ await switchView(win, "locale");
+ await testPostponedBarVisibility({ id: LOCALE_ADDON_ID, win, hidden: true });
+
+ info("Test postponed bar visibility in extension detail view");
+ await switchView(win, "extension");
+ await switchToDetailView({ win, id });
+ await testPostponedBarVisibility({ id, win });
+}
+
+async function completePostponedUpdate({ id, win }) {
+ expectCardOptionsButtonBadged({ id, win, hasBadge: false });
+
+ await testPostponedBarVisibility({ id, win });
+
+ let addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "1", "Addon version is 1");
+
+ const promiseUpdated = waitCardAndAddonUpdated({ id, win });
+ const postponedBar = getCardPostponedBar({ id, win });
+ postponedBar.querySelector("button").click();
+ await promiseUpdated;
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "2", "Addon version is 2");
+
+ await testPostponedBarVisibility({ id, win, hidden: true });
+}
+
+add_task(async function test_pending_update_with_prompted_permission() {
+ const id = "test-pending-update-with-prompted-permission@mochi.test";
+
+ const { extension } = createTestExtension({
+ id,
+ newManifest: { permissions: ["<all_urls>"] },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage-ready");
+
+ const win = await loadInitialView("extension");
+
+ // Force about:addons to check for updates.
+ let promisePermissionHandled = handlePermissionPrompt({
+ addonId: extension.id,
+ assertIcon: false,
+ });
+ win.checkForUpdates();
+ await promisePermissionHandled;
+
+ await promiseUpdateAvailable(extension);
+ await expectAddonInstallStatePostponed(id);
+
+ await completePostponedUpdate({ id, win });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function test_pending_manual_install_over_existing() {
+ const id = "test-pending-manual-install-over-existing@mochi.test";
+
+ const { extension, updateXpi } = createTestExtension({
+ id,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage-ready");
+
+ let win = await loadInitialView("extension");
+
+ info("Manually install new xpi over the existing extension");
+ const promiseInstalled = AddonTestUtils.promiseInstallFile(updateXpi);
+ await promiseUpdateAvailable(extension);
+
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ info("Test postponed bar visibility after reopening about:addons");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ await completePostponedUpdate({ id, win });
+ await promiseInstalled;
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function test_pending_update_no_prompted_permission() {
+ const id = "test-pending-update-no-prompted-permission@mochi.test";
+
+ const { extension } = createTestExtension({ id });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage-ready");
+
+ let win = await loadInitialView("extension");
+
+ info("Force about:addons to check for updates");
+ win.checkForUpdates();
+ await promiseUpdateAvailable(extension);
+
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ info("Test postponed bar visibility after reopening about:addons");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ await completePostponedUpdate({ id, win });
+
+ info("Reopen about:addons again and verify postponed bar hidden");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ await testPostponedBarVisibility({ id, win, hidden: true });
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js
new file mode 100644
index 0000000000..66ce20b989
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js
@@ -0,0 +1,180 @@
+/* eslint max-len: ["error", 80] */
+let gProvider;
+
+function dateHoursAgo(hours) {
+ let date = new Date();
+ date.setTime(date.getTime() - hours * 3600000);
+ return date;
+}
+
+add_task(async function enableHtmlViews() {
+ gProvider = new MockProvider();
+ gProvider.createAddons([
+ {
+ id: "addon-today-2@mochi.test",
+ name: "Updated today two",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(6),
+ },
+ {
+ id: "addon-today-3@mochi.test",
+ name: "Updated today three",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(9),
+ },
+ {
+ id: "addon-today-1@mochi.test",
+ name: "Updated today",
+ creator: { name: "The creator" },
+ version: "3.1",
+ type: "extension",
+ releaseNotesURI: "http://example.com/notes.txt",
+ updateDate: dateHoursAgo(1),
+ },
+ {
+ id: "addon-yesterday-1@mochi.test",
+ name: "Updated yesterday one",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(15),
+ },
+ {
+ id: "addon-earlier@mochi.test",
+ name: "Updated earlier",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(49),
+ },
+ {
+ id: "addon-yesterday-2@mochi.test",
+ name: "Updated yesterday",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(24),
+ },
+ {
+ id: "addon-lastweek@mochi.test",
+ name: "Updated last week",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(192),
+ },
+ ]);
+});
+
+add_task(async function testRecentUpdatesList() {
+ // Load extension view first so we can mock the startOfDay property.
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let categoryUtils = new CategoryUtilities(win);
+ const RECENT_URL = "addons://updates/recent";
+ let recentCat = categoryUtils.get("recent-updates");
+
+ ok(recentCat.hidden, "Recent updates category is initially hidden");
+
+ // Load the recent updates view.
+ let loaded = waitForViewLoad(win);
+ doc.querySelector('#page-options [action="view-recent-updates"]').click();
+ await loaded;
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ RECENT_URL,
+ "Recent updates is selected"
+ );
+ ok(!recentCat.hidden, "Recent updates category is now shown");
+
+ // Find all the add-on ids.
+ let list = doc.querySelector("addon-list");
+ let addonsInOrder = () =>
+ Array.from(list.querySelectorAll("addon-card"))
+ .map(card => card.addon.id)
+ .filter(id => id.endsWith("@mochi.test"));
+
+ // Verify that the add-ons are in the right order.
+ Assert.deepEqual(
+ addonsInOrder(),
+ [
+ "addon-today-1@mochi.test",
+ "addon-today-2@mochi.test",
+ "addon-today-3@mochi.test",
+ "addon-yesterday-1@mochi.test",
+ "addon-yesterday-2@mochi.test",
+ ],
+ "The add-ons are in the right order"
+ );
+
+ info("Check that release notes are shown on the details page");
+ let card = list.querySelector(
+ 'addon-card[addon-id="addon-today-1@mochi.test"]'
+ );
+ loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+ ok(card.expanded, "The card is expanded");
+ ok(!card.details.tabGroup.hidden, "The tabs are shown");
+ ok(
+ !card.details.tabGroup.querySelector('[name="release-notes"]').hidden,
+ "The release notes button is shown"
+ );
+
+ info("Go back to the recent updates view");
+ loaded = waitForViewLoad(win);
+ doc.querySelector('#page-options [action="view-recent-updates"]').click();
+ await loaded;
+
+ // Find the list again.
+ list = doc.querySelector("addon-list");
+
+ info("Install a new add-on, it should be first in the list");
+ // Install a new extension.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "New extension",
+ browser_specific_settings: { gecko: { id: "new@mochi.test" } },
+ },
+ useAddonManager: "temporary",
+ });
+ let added = BrowserTestUtils.waitForEvent(list, "add");
+ await extension.startup();
+ await added;
+
+ // The new extension should now be at the top of the list.
+ Assert.deepEqual(
+ addonsInOrder(),
+ [
+ "new@mochi.test",
+ "addon-today-1@mochi.test",
+ "addon-today-2@mochi.test",
+ "addon-today-3@mochi.test",
+ "addon-yesterday-1@mochi.test",
+ "addon-yesterday-2@mochi.test",
+ ],
+ "The new add-on went to the top"
+ );
+
+ // Open the detail view for the new add-on.
+ card = list.querySelector('addon-card[addon-id="new@mochi.test"]');
+ loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/extension",
+ "The extensions category is selected"
+ );
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js b/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js
new file mode 100644
index 0000000000..045e58d706
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const SUPPORT_URL = "http://support.allizom.org/support-dummy/";
+const SUMO_URL = SUPPORT_URL + "add-on-badges";
+const SUPPORTED_BADGES = ["recommended", "line", "verified"];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["app.support.baseURL", SUPPORT_URL]],
+ });
+});
+
+const server = AddonTestUtils.createHttpServer({
+ hosts: ["support.allizom.org"],
+});
+server.registerPathHandler("/support-dummy", (request, response) => {
+ response.write("Dummy");
+});
+
+async function checkRecommendedBadge(id, badges = []) {
+ async function checkBadge() {
+ let card = win.document.querySelector(`addon-card[addon-id="${id}"]`);
+ for (let badgeName of SUPPORTED_BADGES) {
+ let badge = card.querySelector(`.addon-badge-${badgeName}`);
+ let hidden = !badges.includes(badgeName);
+ is(
+ badge.hidden,
+ hidden,
+ `badge ${badgeName} is ${hidden ? "hidden" : "shown"}`
+ );
+ // Verify the utm params.
+ ok(
+ badge.href.startsWith(SUMO_URL),
+ "links to sumo correctly " + badge.href
+ );
+ if (!hidden) {
+ info(`Verify the ${badgeName} badge links to the support page`);
+ let tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, badge.href);
+ EventUtils.synthesizeMouseAtCenter(badge, {}, win);
+ BrowserTestUtils.removeTab(await tabLoaded);
+ }
+ let url = new URL(badge.href);
+ is(
+ url.searchParams.get("utm_content"),
+ "promoted-addon-badge",
+ "content param correct"
+ );
+ is(
+ url.searchParams.get("utm_source"),
+ "firefox-browser",
+ "source param correct"
+ );
+ is(
+ url.searchParams.get("utm_medium"),
+ "firefox-browser",
+ "medium param correct"
+ );
+ }
+ for (let badgeName of badges) {
+ if (!SUPPORTED_BADGES.includes(badgeName)) {
+ ok(
+ !card.querySelector(`.addon-badge-${badgeName}`),
+ `no badge element for ${badgeName}`
+ );
+ }
+ }
+ return card;
+ }
+
+ let win = await loadInitialView("extension");
+
+ // Check list view.
+ let card = await checkBadge();
+
+ // Load detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Check detail view.
+ await checkBadge();
+
+ await closeView(win);
+}
+
+add_task(async function testNotRecommended() {
+ let id = "not-recommended@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ await checkRecommendedBadge(id);
+
+ await extension.unload();
+});
+
+async function test_badged_addon(addon) {
+ let provider = new MockProvider();
+ provider.createAddons([addon]);
+ await checkRecommendedBadge(addon.id, addon.recommendationStates);
+
+ provider.unregister();
+}
+
+add_task(async function testRecommended() {
+ await test_badged_addon({
+ id: "recommended@mochi.test",
+ isRecommended: true,
+ recommendationStates: ["recommended"],
+ name: "Recommended",
+ type: "extension",
+ });
+});
+
+add_task(async function testLine() {
+ await test_badged_addon({
+ id: "line@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["line"],
+ name: "Line",
+ type: "extension",
+ });
+});
+
+add_task(async function testVerified() {
+ await test_badged_addon({
+ id: "verified@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["verified"],
+ name: "Verified",
+ type: "extension",
+ });
+});
+
+add_task(async function testOther() {
+ await test_badged_addon({
+ id: "other@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["other"],
+ name: "No Badge",
+ type: "extension",
+ });
+});
+
+add_task(async function testMultiple() {
+ await test_badged_addon({
+ id: "multiple@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["verified", "recommended", "other"],
+ name: "Multiple",
+ type: "extension",
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js
new file mode 100644
index 0000000000..e4d88bc19a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js
@@ -0,0 +1,229 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+const server = AddonTestUtils.createHttpServer();
+const TEST_API_URL = `http://localhost:${server.identity.primaryPort}/discoapi`;
+
+const EXT_ID_EXTENSION = "extension@example.com";
+const EXT_ID_THEME = "theme@example.com";
+
+let requestCount = 0;
+server.registerPathHandler("/discoapi", (request, response) => {
+ // This test is expected to load the results only once, and then cache the
+ // results.
+ is(++requestCount, 1, "Expect only one discoapi request");
+
+ let results = {
+ results: [
+ {
+ addon: {
+ authors: [{ name: "Some author" }],
+ current_version: {
+ files: [{ platform: "all", url: "data:," }],
+ },
+ url: "data:,",
+ guid: "recommendation@example.com",
+ type: "extension",
+ },
+ },
+ ],
+ };
+ response.write(JSON.stringify(results));
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.getAddons.discovery.api_url", TEST_API_URL]],
+ });
+
+ let mockProvider = new MockProvider();
+ mockProvider.createAddons([
+ {
+ id: EXT_ID_EXTENSION,
+ name: "Mock 1",
+ type: "extension",
+ userPermissions: {
+ origins: ["<all_urls>"],
+ permissions: ["tabs"],
+ },
+ },
+ {
+ id: EXT_ID_THEME,
+ name: "Mock 2",
+ type: "theme",
+ },
+ ]);
+});
+
+async function switchToView(win, type, param = "") {
+ let loaded = waitForViewLoad(win);
+ win.gViewController.loadView(`addons://${type}/${param}`);
+ await loaded;
+ await waitForStableLayout(win);
+}
+
+// delta = -1 = go back.
+// delta = +1 = go forwards.
+async function historyGo(win, delta, expectedViewType) {
+ let loaded = waitForViewLoad(win);
+ win.history.go(delta);
+ await loaded;
+ is(
+ win.gViewController.currentViewId,
+ expectedViewType,
+ "Expected view after history navigation"
+ );
+ await waitForStableLayout(win);
+}
+
+async function waitForStableLayout(win) {
+ // In the test, it is important that the layout is fully stable before we
+ // consider the view loaded, because those affect the offset calculations.
+ await TestUtils.waitForCondition(
+ () => isLayoutStable(win),
+ "Waiting for layout to stabilize"
+ );
+}
+
+function isLayoutStable(win) {
+ // <message-bar> elements may affect the layout of a page, and therefore we
+ // should check whether its embedded style sheet has finished loading.
+ for (let bar of win.document.querySelectorAll("message-bar")) {
+ // Check for the existence of a CSS property from message-bar.css.
+ if (!win.getComputedStyle(bar).getPropertyValue("--message-bar-icon-url")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function getScrollOffset(win) {
+ let { scrollTop: top, scrollLeft: left } = win.document.documentElement;
+ return { top, left };
+}
+
+// Scroll an element into view. The purpose of this is to simulate a real-world
+// scenario where the user has moved part of the UI is in the viewport.
+function scrollTopLeftIntoView(elem) {
+ elem.scrollIntoView({ block: "start", inline: "start" });
+ // Sanity check: In this test, a large padding has been added to the top and
+ // left of the document. So when an element has been scrolled into view, the
+ // top and left offsets must be non-zero.
+ assertNonZeroScrollOffsets(getScrollOffset(elem.ownerGlobal));
+}
+
+function assertNonZeroScrollOffsets(offsets) {
+ ok(offsets.left, "Should have scrolled to the right");
+ ok(offsets.top, "Should have scrolled down");
+}
+
+function checkScrollOffset(win, expected, msg = "") {
+ let actual = getScrollOffset(win);
+ let fuzz = AppConstants.platform == "macosx" ? 3 : 1;
+ isfuzzy(actual.top, expected.top, fuzz, `Top scroll offset - ${msg}`);
+ isfuzzy(actual.left, expected.left, fuzz, `Left scroll offset - ${msg}`);
+}
+
+add_task(async function test_scroll_restoration() {
+ let win = await loadInitialView("discover");
+
+ // Wait until the recommendations have been loaded. These are cached after
+ // the first load, so we only need to wait once, at the start of the test.
+ await win.document.querySelector("recommended-addon-list").cardsReady;
+
+ // Force scrollbar to appear, by adding enough space around the content.
+ win.document.body.style.paddingBlock = "100vh";
+ win.document.body.style.paddingInline = "100vw";
+ win.document.body.style.width = "300vw";
+
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial page load");
+
+ scrollTopLeftIntoView(win.document.querySelector("recommended-addon-card"));
+ let discoOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(discoOffsets);
+
+ // Switch from disco pane to extension list
+
+ await switchToView(win, "list", "extension");
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial extension list");
+
+ scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION));
+ let extListOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(extListOffsets);
+
+ // Switch from extension list to details view.
+
+ let loaded = waitForViewLoad(win);
+ const addonCard = getAddonCard(win, EXT_ID_EXTENSION);
+ // Ensure that we send a click on the control that is accessible (while a
+ // mouse user could also activate a card by clicking on the entire container):
+ const addonCardLink = addonCard.querySelector(".addon-name-link");
+ addonCardLink.click();
+ await loaded;
+
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial details view");
+ scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION));
+ let detailsOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(detailsOffsets);
+
+ // Switch from details view back to extension list.
+
+ await historyGo(win, -1, "addons://list/extension");
+ checkScrollOffset(win, extListOffsets, "back to extension list");
+
+ // Now scroll to the bottom-right corner, so we can check whether the scroll
+ // offset is correctly restored when the extension view is loaded, even when
+ // the recommendations are loaded after the initial render.
+ ok(
+ win.document.querySelector("recommended-addon-card"),
+ "Recommendations have already been loaded"
+ );
+ win.document.body.scrollIntoView({ block: "end", inline: "end" });
+ extListOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(extListOffsets);
+
+ // Switch back from the extension list to the details view.
+
+ await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`);
+ checkScrollOffset(win, detailsOffsets, "details view with default tab");
+
+ // Switch from the default details tab to the permissions tab.
+ // (this does not change the history).
+ win.document.querySelector(".tab-button[name='permissions']").click();
+
+ // Switch back from the details view to the extension list.
+
+ await historyGo(win, -1, "addons://list/extension");
+ checkScrollOffset(win, extListOffsets, "bottom-right of extension list");
+ ok(
+ win.document.querySelector("recommended-addon-card"),
+ "Recommendations should have been loaded again"
+ );
+
+ // Switch back from extension list to the details view.
+
+ await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`);
+ // Scroll offsets are not remembered for the details view, because at the
+ // time of leaving the details view, the non-default tab was selected.
+ checkScrollOffset(win, { top: 0, left: 0 }, "details view, non-default tab");
+
+ // Switch back from the details view to the disco pane.
+
+ await historyGo(win, -2, "addons://discover/");
+ checkScrollOffset(win, discoOffsets, "after switching back to disco pane");
+
+ // Switch from disco pane to theme list.
+
+ // Verifies that the extension list and theme lists are independent.
+ await switchToView(win, "list", "theme");
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial theme list");
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(gBrowser.selectedTab);
+ await closeView(win);
+ await tabClosed;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js b/toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js
new file mode 100644
index 0000000000..3c8ce5f447
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SITEPERMS_ADDON_PROVIDER_PREF, SITEPERMS_ADDON_TYPE } =
+ ChromeUtils.importESModule(
+ "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs"
+ );
+
+const html = `<!DOCTYPE html><h1>Test midi permission with synthetic site permission addon</h1>`;
+const EXAMPLE_COM_URL = `https://example.com/document-builder.sjs?html=${html}`;
+const EXAMPLE_ORG_URL = `https://example.org/document-builder.sjs?html=${html}`;
+
+async function uninstallAllSitePermissionAddons() {
+ const sitepermAddons = await AddonManager.getAddonsByTypes([
+ SITEPERMS_ADDON_TYPE,
+ ]);
+ for (const addon of sitepermAddons) {
+ await addon.uninstall();
+ }
+}
+
+add_setup(async () => {
+ registerCleanupFunction(uninstallAllSitePermissionAddons);
+});
+
+add_task(async function testAboutAddonUninstall() {
+ if (!AddonManager.hasProvider("SitePermsAddonProvider")) {
+ ok(
+ !Services.prefs.getBoolPref(SITEPERMS_ADDON_PROVIDER_PREF),
+ "Expect SitePermsAddonProvider to be disabled by prefs"
+ );
+ ok(true, "Skip test on SitePermsAddonProvider disabled");
+ return;
+ }
+
+ // Grant midi permission on example.com so about:addons does have a Site Permissions section
+ await SpecialPowers.addPermission("midi-sysex", true, EXAMPLE_COM_URL);
+
+ info("Open an about:addon tab so AMO event listeners are registered");
+ const aboutAddonWin = await loadInitialView("sitepermission");
+ // loadInitialView sets the about:addon as the active one, so we can grab it here.
+ const aboutAddonTab = gBrowser.selectedTab;
+
+ const addonList = aboutAddonWin.document.querySelector("addon-list");
+ let addonCards = aboutAddonWin.document.querySelectorAll("addon-card");
+ is(
+ addonCards.length,
+ 1,
+ "There's a card displayed for the example.com addon"
+ );
+
+ info("Open an example.org tab and install the midi site permission addon");
+ const exampleOrgTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EXAMPLE_ORG_URL,
+ true /* waitForLoad */
+ );
+
+ let promiseAddonCardAdded = BrowserTestUtils.waitForEvent(addonList, "add");
+
+ info("Install midi");
+ await testInstallGatedPermission(
+ exampleOrgTab,
+ () => {
+ content.navigator.requestMIDIAccess();
+ },
+ "midi"
+ );
+
+ info("Install midi-sysex as well");
+ const newAddon = await testInstallGatedPermission(
+ exampleOrgTab,
+ () => {
+ content.navigator.requestMIDIAccess({ sysex: true });
+ },
+ "midi-sysex"
+ );
+
+ const newAddonId = newAddon.id;
+ ok(
+ newAddonId,
+ "Got the addon id for the newly installed sitepermission add-on"
+ );
+
+ info("Switch back to about:addon");
+ gBrowser.selectedTab = aboutAddonTab;
+
+ await promiseAddonCardAdded;
+
+ is(
+ aboutAddonWin.document.querySelectorAll("addon-card").length,
+ 2,
+ "A new addon card has been added as expected"
+ );
+
+ const exampleOrgAddonCard = getAddonCard(aboutAddonWin, newAddonId);
+
+ info("Remove the example.org addon");
+ const promptService = mockPromptService();
+ promptService._response = 0;
+
+ let promiseRemoved = BrowserTestUtils.waitForEvent(addonList, "remove");
+ exampleOrgAddonCard.querySelector("[action=remove]").click();
+ await promiseRemoved;
+
+ is(
+ aboutAddonWin.document.querySelectorAll("addon-card").length,
+ 1,
+ "addon card has been removed as expected"
+ );
+
+ ok(
+ await SpecialPowers.testPermission(
+ "midi",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: EXAMPLE_ORG_URL }
+ ),
+ "midi permission was revoked"
+ );
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: EXAMPLE_ORG_URL }
+ ),
+ "midi-sysex permission was revoked as well"
+ );
+
+ await BrowserTestUtils.removeTab(exampleOrgTab);
+ await close_manager(aboutAddonWin);
+ await uninstallAllSitePermissionAddons();
+});
+
+/**
+ *
+ * Execute a function in the tab content page and check that the expected gated permission
+ * is set
+ *
+ * @param {Tab} tab: The tab in which we want to install the gated permission
+ * @param {Function} spawnCallback: function used in `SpecialPowers.spawn` that will trigger
+ * the install
+ * @param {String} expectedPermType: The name of the permission that should be granted
+ * @returns {Promise<Addon>} The installed addon instance
+ */
+async function testInstallGatedPermission(
+ tab,
+ spawnCallback,
+ expectedPermType
+) {
+ let onInstallEnded = AddonTestUtils.promiseInstallEvent("onInstallEnded");
+ let onAddonInstallBlockedNotification = promisePopupNotificationShown(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], spawnCallback);
+
+ let addonInstallPanel = await onAddonInstallBlockedNotification;
+ let dialogPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addonInstallPanel.button.click();
+ let installPermsDialog = await dialogPromise;
+ installPermsDialog.button.click();
+
+ const addon = await onInstallEnded.then(install => install[0].addon);
+ // Close the addon-installed dialog to avoid interfering with other tests
+ await acceptAppMenuNotificationWhenShown("addon-installed", addon.id);
+
+ ok(
+ await SpecialPowers.testPermission(
+ expectedPermType,
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_ORG_URL }
+ ),
+ `"${expectedPermType}" permission was granted`
+ );
+ return addon;
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
new file mode 100644
index 0000000000..78ffc5678c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
@@ -0,0 +1,750 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const server = AddonTestUtils.createHttpServer();
+
+const initialAutoUpdate = AddonManager.autoUpdateDefault;
+registerCleanupFunction(() => {
+ AddonManager.autoUpdateDefault = initialAutoUpdate;
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.checkUpdateSecurity", false]],
+ });
+
+ Services.telemetry.clearEvents();
+ registerCleanupFunction(() => {
+ cleanupPendingNotifications();
+ });
+});
+
+function loadDetailView(win, id) {
+ let doc = win.document;
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(
+ card.querySelector(".addon-name-link"),
+ { clickCount: 1 },
+ win
+ );
+ return loaded;
+}
+
+add_task(async function testChangeAutoUpdates() {
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test",
+ browser_specific_settings: { gecko: { id } },
+ },
+ // Use permanent so the add-on can be updated.
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+ let addon = await AddonManager.getAddonByID(id);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let getInputs = updateRow => ({
+ default: updatesRow.querySelector('input[value="1"]'),
+ on: updatesRow.querySelector('input[value="2"]'),
+ off: updatesRow.querySelector('input[value="0"]'),
+ checkForUpdate: updatesRow.querySelector('[action="update-check"]'),
+ });
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ ok(card.querySelector("addon-details"), "The card now has details");
+
+ let updatesRow = card.querySelector(".addon-detail-row-updates");
+ let inputs = getInputs(updatesRow);
+ is(addon.applyBackgroundUpdates, 1, "Default is set");
+ ok(inputs.default.checked, "The default option is selected");
+ ok(inputs.checkForUpdate.hidden, "Update check is hidden");
+
+ inputs.on.click();
+ is(addon.applyBackgroundUpdates, "2", "Updates are now enabled");
+ ok(inputs.on.checked, "The on option is selected");
+ ok(inputs.checkForUpdate.hidden, "Update check is hidden");
+
+ inputs.off.click();
+ is(addon.applyBackgroundUpdates, "0", "Updates are now disabled");
+ ok(inputs.off.checked, "The off option is selected");
+ ok(!inputs.checkForUpdate.hidden, "Update check is visible");
+
+ // Go back to the list view and check the details view again.
+ let loaded = waitForViewLoad(win);
+ doc.querySelector(".back-button").click();
+ await loaded;
+
+ // Load the detail view again.
+ await loadDetailView(win, id);
+
+ card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ updatesRow = card.querySelector(".addon-detail-row-updates");
+ inputs = getInputs(updatesRow);
+
+ ok(inputs.off.checked, "Off is still selected");
+
+ // Disable global updates.
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ AddonManager.autoUpdateDefault = false;
+ await updated;
+
+ // Updates are still the same.
+ is(addon.applyBackgroundUpdates, "0", "Updates are now disabled");
+ ok(inputs.off.checked, "The off option is selected");
+ ok(!inputs.checkForUpdate.hidden, "Update check is visible");
+
+ // Check default.
+ inputs.default.click();
+ is(addon.applyBackgroundUpdates, "1", "Default is set");
+ ok(inputs.default.checked, "The default option is selected");
+ ok(!inputs.checkForUpdate.hidden, "Update check is visible");
+
+ inputs.on.click();
+ is(addon.applyBackgroundUpdates, "2", "Updates are now enabled");
+ ok(inputs.on.checked, "The on option is selected");
+ ok(inputs.checkForUpdate.hidden, "Update check is hidden");
+
+ // Enable updates again.
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ AddonManager.autoUpdateDefault = true;
+ await updated;
+
+ await closeView(win);
+ await extension.unload();
+});
+
+async function setupExtensionWithUpdate(
+ id,
+ { releaseNotes, cancelUpdate } = {}
+) {
+ let serverHost = `http://localhost:${server.identity.primaryPort}`;
+ let updatesPath = `/ext-updates-${id}.json`;
+
+ let baseManifest = {
+ name: "Updates",
+ icons: { 48: "an-icon.png" },
+ browser_specific_settings: {
+ gecko: {
+ id,
+ update_url: serverHost + updatesPath,
+ },
+ },
+ };
+
+ let updateXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ ...baseManifest,
+ version: "2",
+ // Include a permission in the updated extension, to make
+ // sure that we trigger the permission prompt as expected
+ // (and that we can accept or cancel the update by observing
+ // the underlying observerService notification).
+ permissions: ["http://*.example.com/*"],
+ },
+ });
+
+ let releaseNotesExtra = {};
+ if (releaseNotes) {
+ let notesPath = "/notes.txt";
+ server.registerPathHandler(notesPath, (request, response) => {
+ if (releaseNotes == "ERROR") {
+ response.setStatusLine(null, 404, "Not Found");
+ } else {
+ response.setStatusLine(null, 200, "OK");
+ response.write(releaseNotes);
+ }
+ response.processAsync();
+ response.finish();
+ });
+ releaseNotesExtra.update_info_url = serverHost + notesPath;
+ }
+
+ let xpiFilename = `/update-${id}.xpi`;
+ server.registerFile(xpiFilename, updateXpi);
+ AddonTestUtils.registerJSON(server, updatesPath, {
+ addons: {
+ [id]: {
+ updates: [
+ {
+ version: "2",
+ update_link: serverHost + xpiFilename,
+ ...releaseNotesExtra,
+ },
+ ],
+ },
+ },
+ });
+
+ handlePermissionPrompt({ addonId: id, reject: cancelUpdate });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ ...baseManifest,
+ version: "1",
+ },
+ // Use permanent so the add-on can be updated.
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+ return extension;
+}
+
+function disableAutoUpdates(card) {
+ // Check button should be hidden.
+ let updateCheckButton = card.querySelector('button[action="update-check"]');
+ ok(updateCheckButton.hidden, "The button is initially hidden");
+
+ // Disable updates, update check button is now visible.
+ card.querySelector('input[name="autoupdate"][value="0"]').click();
+ ok(!updateCheckButton.hidden, "The button is now visible");
+
+ // There shouldn't be an update shown to the user.
+ assertUpdateState({ card, shown: false });
+}
+
+function checkForUpdate(card, expected) {
+ let updateCheckButton = card.querySelector('button[action="update-check"]');
+ let updateFound = BrowserTestUtils.waitForEvent(card, expected);
+ updateCheckButton.click();
+ return updateFound;
+}
+
+function installUpdate(card, expected) {
+ // Install the update.
+ let updateInstalled = BrowserTestUtils.waitForEvent(card, expected);
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ card.querySelector('panel-item[action="install-update"]').click();
+ return Promise.all([updateInstalled, updated]);
+}
+
+async function findUpdatesForAddonId(id) {
+ let addon = await AddonManager.getAddonByID(id);
+ await new Promise(resolve => {
+ addon.findUpdates(
+ { onUpdateAvailable: resolve },
+ AddonManager.UPDATE_WHEN_USER_REQUESTED
+ );
+ });
+}
+
+function assertUpdateState({
+ card,
+ shown,
+ expanded = true,
+ releaseNotes = false,
+}) {
+ let menuButton = card.querySelector(".more-options-button");
+ Assert.equal(
+ menuButton.classList.contains("more-options-button-badged"),
+ shown,
+ "The menu button is badged"
+ );
+ let installButton = card.querySelector('panel-item[action="install-update"]');
+ Assert.notEqual(
+ installButton.hidden,
+ shown,
+ `The install button is ${shown ? "hidden" : "shown"}`
+ );
+ if (expanded) {
+ let updateCheckButton = card.querySelector('button[action="update-check"]');
+ Assert.equal(
+ updateCheckButton.hidden,
+ shown,
+ `The update check button is ${shown ? "hidden" : "shown"}`
+ );
+
+ let { tabGroup } = card.details;
+ is(tabGroup.hidden, false, "The tab group is shown");
+ let notesBtn = tabGroup.querySelector('[name="release-notes"]');
+ is(
+ notesBtn.hidden,
+ !releaseNotes,
+ `The release notes button is ${releaseNotes ? "shown" : "hidden"}`
+ );
+ }
+}
+
+add_task(async function testUpdateAvailable() {
+ let id = "update@mochi.test";
+ let extension = await setupExtensionWithUpdate(id);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector("addon-card");
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true });
+
+ // The version was 1.
+ let versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "1", "The version started as 1");
+
+ await installUpdate(card, "update-installed");
+
+ // The version is now 2.
+ versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "2", "The version has updated");
+
+ // No update is shown again.
+ assertUpdateState({ card, shown: false });
+
+ // Check for updates again, there shouldn't be an update.
+ await checkForUpdate(card, "no-update");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testReleaseNotesLoad() {
+ Services.telemetry.clearEvents();
+ let id = "update-with-notes@mochi.test";
+ let extension = await setupExtensionWithUpdate(id, {
+ releaseNotes: `
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head><link rel="stylesheet" href="remove-me.css"/></head>
+ <body>
+ <script src="no-scripts.js"></script>
+ <h1>My release notes</h1>
+ <img src="http://example.com/tracker.png"/>
+ <ul>
+ <li onclick="alert('hi')">A thing</li>
+ </ul>
+ <a href="http://example.com/">Go somewhere</a>
+ </body>
+ </html>
+ `,
+ });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector("addon-card");
+ let { deck, tabGroup } = card.details;
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true, releaseNotes: true });
+
+ info("Check release notes");
+ let notesBtn = tabGroup.querySelector('[name="release-notes"]');
+ let notes = card.querySelector("update-release-notes");
+ let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading");
+ let loaded = BrowserTestUtils.waitForEvent(notes, "release-notes-loaded");
+ // Don't use notesBtn.click() since it causes an assertion to fail.
+ // See bug 1551621 for more info.
+ EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win);
+ await loading;
+ is(
+ doc.l10n.getAttributes(notes.firstElementChild).id,
+ "release-notes-loading",
+ "The loading message is shown"
+ );
+ await loaded;
+ info("Checking HTML release notes");
+ let [h1, ul, a] = notes.children;
+ is(h1.tagName, "H1", "There's a heading");
+ is(h1.textContent, "My release notes", "The heading has content");
+ is(ul.tagName, "UL", "There's a list");
+ is(ul.children.length, 1, "There's one item in the list");
+ let [li] = ul.children;
+ is(li.tagName, "LI", "There's a list item");
+ is(li.textContent, "A thing", "The text is set");
+ ok(!li.hasAttribute("onclick"), "The onclick was removed");
+ ok(!notes.querySelector("link"), "The link tag was removed");
+ ok(!notes.querySelector("script"), "The script tag was removed");
+ is(a.textContent, "Go somewhere", "The link text is preserved");
+ is(a.href, "http://example.com/", "The link href is preserved");
+
+ info("Verify the link opened in a new tab");
+ let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, a.href);
+ a.click();
+ let tab = await tabOpened;
+ BrowserTestUtils.removeTab(tab);
+
+ let originalContent = notes.innerHTML;
+
+ info("Switch away and back to release notes");
+ // Load details view.
+ let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]');
+ let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ detailsBtn.click();
+ await viewChanged;
+
+ // Load release notes again, verify they weren't loaded.
+ viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ let notesCached = BrowserTestUtils.waitForEvent(
+ notes,
+ "release-notes-cached"
+ );
+ notesBtn.click();
+ await viewChanged;
+ await notesCached;
+ is(notes.innerHTML, originalContent, "The content didn't change");
+
+ info("Install the update to clean it up");
+ await installUpdate(card, "update-installed");
+
+ // There's no more update but release notes are still shown.
+ assertUpdateState({ card, shown: false, releaseNotes: true });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testReleaseNotesError() {
+ let id = "update-with-notes-error@mochi.test";
+ let extension = await setupExtensionWithUpdate(id, { releaseNotes: "ERROR" });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector("addon-card");
+ let { deck, tabGroup } = card.details;
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true, releaseNotes: true });
+
+ info("Check release notes");
+ let notesBtn = tabGroup.querySelector('[name="release-notes"]');
+ let notes = card.querySelector("update-release-notes");
+ let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading");
+ let errored = BrowserTestUtils.waitForEvent(notes, "release-notes-error");
+ // Don't use notesBtn.click() since it causes an assertion to fail.
+ // See bug 1551621 for more info.
+ EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win);
+ await loading;
+ is(
+ doc.l10n.getAttributes(notes.firstElementChild).id,
+ "release-notes-loading",
+ "The loading message is shown"
+ );
+ await errored;
+ is(
+ doc.l10n.getAttributes(notes.firstElementChild).id,
+ "release-notes-error",
+ "The error message is shown"
+ );
+
+ info("Switch away and back to release notes");
+ // Load details view.
+ let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]');
+ let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ detailsBtn.click();
+ await viewChanged;
+
+ // Load release notes again, verify they weren't loaded.
+ viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ let notesCached = BrowserTestUtils.waitForEvent(
+ notes,
+ "release-notes-cached"
+ );
+ notesBtn.click();
+ await viewChanged;
+ await notesCached;
+
+ info("Install the update to clean it up");
+ await installUpdate(card, "update-installed");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testUpdateCancelled() {
+ let id = "update@mochi.test";
+ let extension = await setupExtensionWithUpdate(id, { cancelUpdate: true });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, "update@mochi.test");
+ let card = doc.querySelector("addon-card");
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true });
+
+ // The add-on starts as version 1.
+ let versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "1", "The version started as 1");
+
+ // Force the install to be cancelled.
+ let install = card.updateInstall;
+ ok(install, "There was an install found");
+
+ await installUpdate(card, "update-cancelled");
+
+ // The add-on is still version 1.
+ versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "1", "The version hasn't changed");
+
+ // The update has been removed.
+ assertUpdateState({ card, shown: false });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testAvailableUpdates() {
+ let ids = ["update1@mochi.test", "update2@mochi.test", "update3@mochi.test"];
+ let addons = await Promise.all(ids.map(id => setupExtensionWithUpdate(id)));
+
+ // Disable global add-on updates.
+ AddonManager.autoUpdateDefault = false;
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let updatesMessage = doc.getElementById("updates-message");
+ let categoryUtils = new CategoryUtilities(win);
+
+ let availableCat = categoryUtils.get("available-updates");
+
+ ok(availableCat.hidden, "Available updates is hidden");
+ is(availableCat.badgeCount, 0, "There are no updates");
+ ok(updatesMessage, "There is an updates message");
+ is_element_hidden(updatesMessage, "The message is hidden");
+ ok(!updatesMessage.message.textContent, "The message is empty");
+ ok(!updatesMessage.button.textContent, "The button is empty");
+
+ // Check for all updates.
+ let updatesFound = TestUtils.topicObserved("EM-update-check-finished");
+ doc.querySelector('#page-options [action="check-for-updates"]').click();
+
+ is_element_visible(updatesMessage, "The message is visible");
+ ok(!updatesMessage.message.textContent, "The message is empty");
+ ok(updatesMessage.button.hidden, "The view updates button is hidden");
+
+ // Make sure the message gets populated by fluent.
+ await TestUtils.waitForCondition(
+ () => updatesMessage.message.textContent,
+ "wait for message text"
+ );
+
+ await updatesFound;
+
+ // The button should be visible, and should get some text from fluent.
+ ok(!updatesMessage.button.hidden, "The view updates button is visible");
+ await TestUtils.waitForCondition(
+ () => updatesMessage.button.textContent,
+ "wait for button text"
+ );
+
+ // Wait for the available updates count to finalize, it's async.
+ await BrowserTestUtils.waitForCondition(() => availableCat.badgeCount == 3);
+
+ // The category shows the correct update count.
+ ok(!availableCat.hidden, "Available updates is visible");
+ is(availableCat.badgeCount, 3, "There are 3 updates");
+
+ // Go to the available updates page.
+ let loaded = waitForViewLoad(win);
+ availableCat.click();
+ await loaded;
+
+ // Check the updates are shown.
+ let cards = doc.querySelectorAll("addon-card");
+ is(cards.length, 3, "There are 3 cards");
+
+ // Each card should have an update.
+ for (let card of cards) {
+ assertUpdateState({ card, shown: true, expanded: false });
+ }
+
+ // Check the detail page for the first add-on.
+ await loadDetailView(win, ids[0]);
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/extension",
+ "The extensions category is selected"
+ );
+
+ // Go back to the last view.
+ loaded = waitForViewLoad(win);
+ doc.querySelector(".back-button").click();
+ await loaded;
+
+ // We're back on the updates view.
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://updates/available",
+ "The available updates category is selected"
+ );
+
+ // Find the cards again.
+ cards = doc.querySelectorAll("addon-card");
+ is(cards.length, 3, "There are 3 cards");
+
+ // Install the first update.
+ await installUpdate(cards[0], "update-installed");
+ assertUpdateState({ card: cards[0], shown: false, expanded: false });
+
+ // The count goes down but the card stays.
+ is(availableCat.badgeCount, 2, "There are only 2 updates now");
+ is(
+ doc.querySelectorAll("addon-card").length,
+ 3,
+ "All 3 cards are still visible on the updates page"
+ );
+
+ // Install the other two updates.
+ await installUpdate(cards[1], "update-installed");
+ assertUpdateState({ card: cards[1], shown: false, expanded: false });
+ await installUpdate(cards[2], "update-installed");
+ assertUpdateState({ card: cards[2], shown: false, expanded: false });
+
+ // The count goes down but the card stays.
+ is(availableCat.badgeCount, 0, "There are no more updates");
+ is(
+ doc.querySelectorAll("addon-card").length,
+ 3,
+ "All 3 cards are still visible on the updates page"
+ );
+
+ // Enable global add-on updates again.
+ AddonManager.autoUpdateDefault = true;
+
+ await closeView(win);
+ await Promise.all(addons.map(addon => addon.unload()));
+});
+
+add_task(async function testUpdatesShownOnLoad() {
+ let id = "has-update@mochi.test";
+ let addon = await setupExtensionWithUpdate(id);
+
+ // Find the update for our addon.
+ AddonManager.autoUpdateDefault = false;
+ await findUpdatesForAddonId(id);
+
+ let win = await loadInitialView("extension");
+ let categoryUtils = new CategoryUtilities(win);
+ let updatesButton = categoryUtils.get("available-updates");
+
+ ok(!updatesButton.hidden, "The updates button is shown");
+ is(updatesButton.badgeCount, 1, "There is an update");
+
+ let loaded = waitForViewLoad(win);
+ updatesButton.click();
+ await loaded;
+
+ let cards = win.document.querySelectorAll("addon-card");
+
+ is(cards.length, 1, "There is one update card");
+
+ let card = cards[0];
+ is(card.addon.id, id, "The update is for the expected add-on");
+
+ await installUpdate(card, "update-installed");
+
+ ok(!updatesButton.hidden, "The updates button is still shown");
+ is(updatesButton.badgeCount, 0, "There are no more updates");
+
+ info("Check that the updates section is hidden when re-opened");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ categoryUtils = new CategoryUtilities(win);
+ updatesButton = categoryUtils.get("available-updates");
+
+ ok(updatesButton.hidden, "Available updates is hidden");
+ is(updatesButton.badgeCount, 0, "There are no updates");
+
+ AddonManager.autoUpdateDefault = true;
+ await closeView(win);
+ await addon.unload();
+});
+
+add_task(async function testPromptOnBackgroundUpdateCheck() {
+ const id = "test-prompt-on-background-check@mochi.test";
+ const extension = await setupExtensionWithUpdate(id);
+
+ AddonManager.autoUpdateDefault = false;
+
+ const addon = await AddonManager.getAddonByID(id);
+ await AddonTestUtils.promiseFindAddonUpdates(
+ addon,
+ AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
+ );
+ let win = await loadInitialView("extension");
+
+ let card = getAddonCard(win, id);
+
+ const promisePromptInfo = promisePermissionPrompt(id);
+ await installUpdate(card, "update-installed");
+ const promptInfo = await promisePromptInfo;
+ ok(promptInfo, "Got a permission prompt as expected");
+
+ AddonManager.autoUpdateDefault = true;
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testNoUpdateAvailableOnUnrelatedAddonCards() {
+ let idNoUpdate = "no-update@mochi.test";
+
+ let extensionNoUpdate = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ name: "TestAddonNoUpdate",
+ browser_specific_settings: { gecko: { id: idNoUpdate } },
+ },
+ });
+ await extensionNoUpdate.startup();
+
+ let win = await loadInitialView("extension");
+
+ let cardNoUpdate = getAddonCard(win, idNoUpdate);
+ ok(cardNoUpdate, `Got AddonCard for ${idNoUpdate}`);
+
+ // Assert that there is not an update badge
+ assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false });
+
+ // Trigger a onNewInstall event by install another unrelated addon.
+ const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+ let install = await AddonManager.getInstallForURL(XPI_URL);
+ await AddonManager.installAddonFromAOM(
+ gBrowser.selectedBrowser,
+ win.document.documentURIObject,
+ install
+ );
+
+ // Cancel the install used to trigger the onNewInstall install event.
+ await install.cancel();
+ // Assert that the previously installed addon isn't marked with the
+ // update available badge after installing an unrelated addon.
+ assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false });
+
+ await closeView(win);
+ await extensionNoUpdate.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js b/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js
new file mode 100644
index 0000000000..de34cff82b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js
@@ -0,0 +1,290 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+let gProvider;
+const { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService;
+
+const appVersion = Services.appinfo.version;
+const SUPPORT_URL = Services.urlFormatter.formatURL(
+ Services.prefs.getStringPref("app.support.baseURL")
+);
+
+add_setup(async function () {
+ gProvider = new MockProvider();
+});
+
+async function checkMessageState(id, addonType, expected) {
+ async function checkAddonCard() {
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ let messageBar = card.querySelector(".addon-card-message");
+
+ if (!expected) {
+ ok(messageBar.hidden, "message is hidden");
+ } else {
+ const { linkUrl, text, type } = expected;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ messageBar,
+ { attributes: true },
+ () => !messageBar.hidden
+ );
+ ok(!messageBar.hidden, "message is visible");
+
+ is(messageBar.getAttribute("type"), type, "message has the right type");
+ Assert.deepEqual(
+ document.l10n.getAttributes(messageBar),
+ { id: `${text.id}2`, args: text.args },
+ "message l10n data is set correctly"
+ );
+
+ const link = messageBar.querySelector("button");
+ if (linkUrl) {
+ ok(!link.hidden, "link is visible");
+ is(
+ link.getAttribute("data-l10n-id"),
+ `${text.id}-link`,
+ "link l10n id is correct"
+ );
+ const newTab = BrowserTestUtils.waitForNewTab(gBrowser, linkUrl);
+ link.click();
+ BrowserTestUtils.removeTab(await newTab);
+ } else {
+ ok(link.hidden, "link is hidden");
+ }
+ }
+
+ return card;
+ }
+
+ let win = await loadInitialView(addonType);
+ let doc = win.document;
+
+ // Check the list view.
+ ok(doc.querySelector("addon-list"), "this is a list view");
+ let card = await checkAddonCard();
+
+ // Load the detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Check the detail view.
+ ok(!doc.querySelector("addon-list"), "this isn't a list view");
+ await checkAddonCard();
+
+ await closeView(win);
+}
+
+add_task(async function testNoMessageExtension() {
+ let id = "no-message@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ await checkMessageState(id, "extension", null);
+
+ await extension.unload();
+});
+
+add_task(async function testNoMessageLangpack() {
+ let id = "no-message@mochi.test";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ name: "Signed Langpack",
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ type: "locale",
+ },
+ ]);
+
+ await checkMessageState(id, "locale", null);
+});
+
+add_task(async function testBlocked() {
+ const id = "blocked@mochi.test";
+ const linkUrl = "https://example.com/addon-blocked";
+ const name = "Blocked";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ blocklistState: STATE_BLOCKED,
+ blocklistURL: linkUrl,
+ id,
+ isActive: false,
+ name,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkUrl,
+ text: { id: "details-notification-blocked", args: { name } },
+ type: "error",
+ });
+});
+
+add_task(async function testUnsignedDisabled() {
+ // This pref being disabled will cause the `specialpowers` addon to be
+ // uninstalled, which can cause a number of test failures due to features no
+ // longer working correctly.
+ // In order to avoid those issues, this code manually disables the pref, and
+ // ensures that `SpecialPowers` is fully re-enabled at the end of the test.
+ const sigPref = "xpinstall.signatures.required";
+ Services.prefs.setBoolPref(sigPref, true);
+
+ const id = "unsigned@mochi.test";
+ const name = "Unsigned";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ name,
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text: { id: "details-notification-unsigned-and-disabled", args: { name } },
+ type: "error",
+ });
+
+ // Ensure that `SpecialPowers` is fully re-initialized at the end of this
+ // test. This requires removing the existing binding so that it's
+ // re-registered, re-enabling unsigned extensions, and then waiting for the
+ // actor to be registered and ready.
+ delete window.SpecialPowers;
+ Services.prefs.setBoolPref(sigPref, false);
+ await TestUtils.waitForCondition(() => {
+ try {
+ return !!windowGlobalChild.getActor("SpecialPowers");
+ } catch (e) {
+ return false;
+ }
+ }, "wait for SpecialPowers to be reloaded");
+ ok(window.SpecialPowers, "SpecialPowers should be re-defined");
+});
+
+add_task(async function testUnsignedLangpackDisabled() {
+ const id = "unsigned-langpack@mochi.test";
+ const name = "Unsigned";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ name,
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ type: "locale",
+ },
+ ]);
+ await checkMessageState(id, "locale", {
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text: { id: "details-notification-unsigned-and-disabled", args: { name } },
+ type: "error",
+ });
+});
+
+add_task(async function testIncompatible() {
+ const id = "incompatible@mochi.test";
+ const name = "Incompatible";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ isActive: false,
+ isCompatible: false,
+ name,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ text: {
+ id: "details-notification-incompatible",
+ args: { name, version: appVersion },
+ },
+ type: "error",
+ });
+});
+
+add_task(async function testUnsignedEnabled() {
+ const id = "unsigned-allowed@mochi.test";
+ const name = "Unsigned";
+ gProvider.createAddons([
+ {
+ id,
+ name,
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text: { id: "details-notification-unsigned", args: { name } },
+ type: "warning",
+ });
+});
+
+add_task(async function testUnsignedLangpackEnabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.langpacks.signatures.required", false]],
+ });
+
+ const id = "unsigned-allowed-langpack@mochi.test";
+ const name = "Unsigned Langpack";
+ gProvider.createAddons([
+ {
+ id,
+ name,
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ type: "locale",
+ },
+ ]);
+ await checkMessageState(id, "locale", {
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text: { id: "details-notification-unsigned", args: { name } },
+ type: "warning",
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testSoftBlocked() {
+ const id = "softblocked@mochi.test";
+ const linkUrl = "https://example.com/addon-blocked";
+ const name = "Soft Blocked";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ blocklistState: STATE_SOFTBLOCKED,
+ blocklistURL: linkUrl,
+ id,
+ isActive: false,
+ name,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkUrl,
+ text: { id: "details-notification-softblocked", args: { name } },
+ type: "warning",
+ });
+});
+
+add_task(async function testPluginInstalling() {
+ const id = "plugin-installing@mochi.test";
+ const name = "Plugin Installing";
+ gProvider.createAddons([
+ {
+ id,
+ isActive: true,
+ isGMPlugin: true,
+ isInstalled: false,
+ name,
+ type: "plugin",
+ },
+ ]);
+ await checkMessageState(id, "plugin", {
+ text: { id: "details-notification-gmp-pending", args: { name } },
+ type: "warning",
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_installssl.js b/toolkit/mozapps/extensions/test/browser/browser_installssl.js
new file mode 100644
index 0000000000..4469b846bf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_installssl.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const xpi = RELATIVE_DIR + "addons/browser_installssl.xpi";
+const redirect = RELATIVE_DIR + "redirect.sjs?";
+const SUCCESS = 0;
+const NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE;
+
+const HTTP = "http://example.com/";
+const HTTPS = "https://example.com/";
+const NOCERT = "https://nocert.example.com/";
+const SELFSIGNED = "https://self-signed.example.com/";
+const UNTRUSTED = "https://untrusted.example.com/";
+const EXPIRED = "https://expired.example.com/";
+
+const PREF_INSTALL_REQUIREBUILTINCERTS =
+ "extensions.install.requireBuiltInCerts";
+
+var gTests = [];
+var gStart = 0;
+var gLast = 0;
+var gPendingInstall = null;
+
+function test() {
+ gStart = Date.now();
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ var cos = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+ cos.clearValidityOverride("nocert.example.com", -1, {});
+ cos.clearValidityOverride("self-signed.example.com", -1, {});
+ cos.clearValidityOverride("untrusted.example.com", -1, {});
+ cos.clearValidityOverride("expired.example.com", -1, {});
+
+ if (gPendingInstall) {
+ gTests = [];
+ ok(
+ false,
+ "Timed out in the middle of downloading " +
+ gPendingInstall.sourceURI.spec
+ );
+ try {
+ gPendingInstall.cancel();
+ } catch (e) {}
+ }
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ info("All tests completed in " + (Date.now() - gStart) + "ms");
+ finish();
+}
+
+function add_install_test(mainURL, redirectURL, expectedStatus) {
+ gTests.push([mainURL, redirectURL, expectedStatus]);
+}
+
+function run_install_tests(callback) {
+ async function run_next_install_test() {
+ if (!gTests.length) {
+ callback();
+ return;
+ }
+ gLast = Date.now();
+
+ let [mainURL, redirectURL, expectedStatus] = gTests.shift();
+ if (redirectURL) {
+ var url = mainURL + redirect + redirectURL + xpi;
+ var message =
+ "Should have seen the right result for an install redirected from " +
+ mainURL +
+ " to " +
+ redirectURL;
+ } else {
+ url = mainURL + xpi;
+ message =
+ "Should have seen the right result for an install from " + mainURL;
+ }
+
+ let install = await AddonManager.getInstallForURL(url);
+ gPendingInstall = install;
+ install.addListener({
+ onDownloadEnded(install) {
+ is(SUCCESS, expectedStatus, message);
+ info("Install test ran in " + (Date.now() - gLast) + "ms");
+ // Don't proceed with the install
+ install.cancel();
+ gPendingInstall = null;
+ run_next_install_test();
+ return false;
+ },
+
+ onDownloadFailed(install) {
+ is(install.error, expectedStatus, message);
+ info("Install test ran in " + (Date.now() - gLast) + "ms");
+ gPendingInstall = null;
+ run_next_install_test();
+ },
+ });
+ install.install();
+ }
+
+ run_next_install_test();
+}
+
+// Runs tests with built-in certificates required, no certificate exceptions
+// and no hashes
+add_test(async function test_builtin_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, true]],
+ });
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, NETWORK_FAILURE);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTP, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, NETWORK_FAILURE);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates, no certificate
+// exceptions and no hashes
+add_test(async function test_builtin_not_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, SUCCESS);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTP, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, SUCCESS);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Set up overrides for the next test.
+add_test(() => {
+ addCertOverrides().then(run_next_test);
+});
+
+// Runs tests with built-in certificates required, all certificate exceptions
+// and no hashes
+add_test(async function test_builtin_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, true]],
+ });
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, NETWORK_FAILURE);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, SUCCESS);
+ add_install_test(HTTP, SELFSIGNED, SUCCESS);
+ add_install_test(HTTP, UNTRUSTED, SUCCESS);
+ add_install_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, NETWORK_FAILURE);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates, all certificate
+// exceptions and no hashes
+add_test(async function test_builtin_not_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, SUCCESS);
+ add_install_test(NOCERT, null, SUCCESS);
+ add_install_test(SELFSIGNED, null, SUCCESS);
+ add_install_test(UNTRUSTED, null, SUCCESS);
+ add_install_test(EXPIRED, null, SUCCESS);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, SUCCESS);
+ add_install_test(HTTP, SELFSIGNED, SUCCESS);
+ add_install_test(HTTP, UNTRUSTED, SUCCESS);
+ add_install_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, SUCCESS);
+ add_install_test(HTTPS, NOCERT, SUCCESS);
+ add_install_test(HTTPS, SELFSIGNED, SUCCESS);
+ add_install_test(HTTPS, UNTRUSTED, SUCCESS);
+ add_install_test(HTTPS, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, SUCCESS);
+ add_install_test(NOCERT, NOCERT, SUCCESS);
+ add_install_test(NOCERT, SELFSIGNED, SUCCESS);
+ add_install_test(NOCERT, UNTRUSTED, SUCCESS);
+ add_install_test(NOCERT, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, SUCCESS);
+ add_install_test(SELFSIGNED, NOCERT, SUCCESS);
+ add_install_test(SELFSIGNED, SELFSIGNED, SUCCESS);
+ add_install_test(SELFSIGNED, UNTRUSTED, SUCCESS);
+ add_install_test(SELFSIGNED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, SUCCESS);
+ add_install_test(UNTRUSTED, NOCERT, SUCCESS);
+ add_install_test(UNTRUSTED, SELFSIGNED, SUCCESS);
+ add_install_test(UNTRUSTED, UNTRUSTED, SUCCESS);
+ add_install_test(UNTRUSTED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, SUCCESS);
+ add_install_test(EXPIRED, NOCERT, SUCCESS);
+ add_install_test(EXPIRED, SELFSIGNED, SUCCESS);
+ add_install_test(EXPIRED, UNTRUSTED, SUCCESS);
+ add_install_test(EXPIRED, EXPIRED, SUCCESS);
+
+ run_install_tests(run_next_test);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js
new file mode 100644
index 0000000000..1d50da2833
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js
@@ -0,0 +1,362 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org";
+
+AddonTestUtils.initMochitest(this);
+
+AddonTestUtils.hookAMTelemetryEvents();
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+
+ PermissionTestUtils.add(
+ "https://example.com/",
+ "install",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(async () => {
+ PermissionTestUtils.remove("https://example.com", "install");
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+async function testInstallTrigger(
+ msg,
+ tabURL,
+ contentFnArgs,
+ contentFn,
+ expectedTelemetryInfo,
+ expectBlockedOrigin
+) {
+ // Clear collected events before each test, otherwise the test would fail
+ // intermittently when Glean is going to submit the events and clear them
+ // after reaching the max events length limit.
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(tabURL, async browser => {
+ if (expectBlockedOrigin) {
+ const promiseOriginBlocked = TestUtils.topicObserved(
+ "addon-install-origin-blocked"
+ );
+ await SpecialPowers.spawn(browser, contentFnArgs, contentFn);
+ const [subject] = await promiseOriginBlocked;
+ const installId = subject.wrappedJSObject.installs[0].installId;
+
+ let gleanEvents = AddonTestUtils.getAMGleanEvents("install", {
+ install_id: `${installId}`,
+ step: "site_blocked",
+ });
+ ok(!!gleanEvents.length, "Found Glean events for the blocked install.");
+ Assert.deepEqual(
+ { source: gleanEvents[0].source },
+ expectedTelemetryInfo,
+ `Got expected Glean telemetry on test case "${msg}"`
+ );
+
+ // Select all telemetry events related to the installId.
+ const telemetryEvents = AddonTestUtils.getAMTelemetryEvents().filter(
+ ev => {
+ return (
+ ev.method === "install" &&
+ ev.value === `${installId}` &&
+ ev.extra.step === "site_blocked"
+ );
+ }
+ );
+ ok(
+ !!telemetryEvents.length,
+ "Found telemetry events for the blocked install"
+ );
+
+ const source = telemetryEvents[0]?.extra.source;
+ Assert.deepEqual(
+ { source },
+ expectedTelemetryInfo,
+ `Got expected telemetry on test case "${msg}"`
+ );
+ return;
+ }
+
+ let installPromptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ let promptPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ XPI_ADDON_ID
+ );
+
+ await SpecialPowers.spawn(browser, contentFnArgs, contentFn);
+
+ await Promise.all([installPromptPromise, promptPromise]);
+
+ let addon = await promiseAddonByID(XPI_ADDON_ID);
+
+ registerCleanupFunction(async () => {
+ await addon.uninstall();
+ });
+
+ // Check that the expected installTelemetryInfo has been stored in the
+ // addon details.
+ AddonTestUtils.checkInstallInfo(
+ addon,
+ { method: "installTrigger", ...expectedTelemetryInfo },
+ `on "${msg}"`
+ );
+
+ await addon.uninstall();
+ });
+}
+
+add_task(function testInstallAfterHistoryPushState() {
+ return testInstallTrigger(
+ "InstallTrigger after history.pushState",
+ SECURE_TESTROOT,
+ [SECURE_TESTROOT, XPI_URL],
+ (secureTestRoot, xpiURL) => {
+ // `sourceURL` should match the exact location, even after a location
+ // update using the history API. In this case, we update the URL with
+ // query parameters and expect `sourceURL` to contain those parameters.
+ content.history.pushState(
+ {}, // state
+ "", // title
+ `${secureTestRoot}?some=query&par=am`
+ );
+ content.InstallTrigger.install({ URL: xpiURL });
+ },
+ {
+ source: "test-host",
+ sourceURL:
+ "https://example.com/browser/toolkit/mozapps/extensions/test/browser/?some=query&par=am",
+ }
+ );
+});
+
+add_task(async function testInstallTriggerFromSubframe() {
+ function runTestCase(msg, tabURL, testFrameAttrs, expected) {
+ info(
+ `InstallTrigger from iframe test: ${msg} - frame attributes ${JSON.stringify(
+ testFrameAttrs
+ )}`
+ );
+ return testInstallTrigger(
+ msg,
+ tabURL,
+ [XPI_URL, testFrameAttrs],
+ async (xpiURL, frameAttrs) => {
+ const frame = content.document.createElement("iframe");
+ if (frameAttrs) {
+ for (const attr of Object.keys(frameAttrs)) {
+ let value = frameAttrs[attr];
+ if (value === "blob:") {
+ const blob = new content.Blob(["blob-testpage"]);
+ value = content.URL.createObjectURL(blob, "text/html");
+ }
+ frame[attr] = value;
+ }
+ }
+ const promiseLoaded = new Promise(resolve =>
+ frame.addEventListener("load", resolve, { once: true })
+ );
+ content.document.body.appendChild(frame);
+ await promiseLoaded;
+ frame.contentWindow.InstallTrigger.install({ URL: xpiURL });
+ },
+ expected.telemetryInfo,
+ expected.blockedOrigin
+ );
+ }
+
+ // On Windows "file:///" does not load the default files index html page
+ // and the test would get stuck.
+ const fileURL = AppConstants.platform === "win" ? "file:///C:/" : "file:///";
+
+ const expected = {
+ http: {
+ telemetryInfo: {
+ source: "test-host",
+ sourceURL:
+ "https://example.com/browser/toolkit/mozapps/extensions/test/browser/",
+ },
+ blockedOrigin: false,
+ },
+ httpBlob: {
+ telemetryInfo: {
+ source: "test-host",
+ // Example: "blob:https://example.com/BLOB_URL_UUID"
+ sourceURL: /^blob:https:\/\/example\.com\//,
+ },
+ blockedOrigin: false,
+ },
+ file: {
+ telemetryInfo: {
+ source: "unknown",
+ sourceURL: fileURL,
+ },
+ blockedOrigin: false,
+ },
+ fileBlob: {
+ telemetryInfo: {
+ source: "unknown",
+ // Example: "blob:null/BLOB_URL_UUID"
+ sourceURL: /^blob:null\//,
+ },
+ blockedOrigin: false,
+ },
+ httpBlockedOnOrigin: {
+ telemetryInfo: {
+ source: "test-host",
+ },
+ blockedOrigin: true,
+ },
+ otherBlockedOnOrigin: {
+ telemetryInfo: {
+ source: "unknown",
+ },
+ blockedOrigin: true,
+ },
+ };
+
+ const testCases = [
+ ["blank iframe with no attributes", SECURE_TESTROOT, {}, expected.http],
+
+ // These are blocked by a Firefox doorhanger and the user can't allow it neither.
+ [
+ "http page iframe src='blob:...'",
+ SECURE_TESTROOT,
+ { src: "blob:" },
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "file page iframe src='blob:...'",
+ fileURL,
+ { src: "blob:" },
+ expected.otherBlockedOnOrigin,
+ ],
+ [
+ "iframe srcdoc=''",
+ SECURE_TESTROOT,
+ { srcdoc: "" },
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "blank iframe embedded into a top-level sandbox page",
+ `${SECURE_TESTROOT}sandboxed.html`,
+ {},
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "blank iframe with sandbox='allow-scripts'",
+ SECURE_TESTROOT,
+ { sandbox: "allow-scripts" },
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "iframe srcdoc='' sandbox='allow-scripts'",
+ SECURE_TESTROOT,
+ { srcdoc: "", sandbox: "allow-scripts" },
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "http page iframe src='blob:...' sandbox='allow-scripts'",
+ SECURE_TESTROOT,
+ { src: "blob:", sandbox: "allow-scripts" },
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "iframe src='data:...'",
+ SECURE_TESTROOT,
+ { src: "data:text/html,data-testpage" },
+ expected.httpBlockedOnOrigin,
+ ],
+ [
+ "blank frame embedded in a data url",
+ "data:text/html,data-testpage",
+ {},
+ expected.otherBlockedOnOrigin,
+ ],
+ [
+ "blank frame embedded into a about:blank page",
+ "about:blank",
+ {},
+ expected.otherBlockedOnOrigin,
+ ],
+ ];
+
+ for (const testCase of testCases) {
+ await runTestCase(...testCase);
+ }
+});
+
+add_task(function testInstallBlankFrameNestedIntoBlobURLPage() {
+ return testInstallTrigger(
+ "Blank frame nested into a blob url page",
+ SECURE_TESTROOT,
+ [XPI_URL],
+ async xpiURL => {
+ const url = content.URL.createObjectURL(
+ new content.Blob(["blob-testpage"]),
+ "text/html"
+ );
+ const topframe = content.document.createElement("iframe");
+ topframe.src = url;
+ const topframeLoaded = new Promise(resolve => {
+ topframe.addEventListener("load", resolve, { once: true });
+ });
+ content.document.body.appendChild(topframe);
+ await topframeLoaded;
+ const subframe = topframe.contentDocument.createElement("iframe");
+ topframe.contentDocument.body.appendChild(subframe);
+ subframe.contentWindow.InstallTrigger.install({ URL: xpiURL });
+ },
+ {
+ source: "test-host",
+ },
+ /* expectBlockedOrigin */ true
+ );
+});
+
+add_task(function testInstallTriggerTopLevelDataURL() {
+ return testInstallTrigger(
+ "Blank frame nested into a blob url page",
+ "data:text/html,testpage",
+ [XPI_URL],
+ async xpiURL => {
+ this.content.InstallTrigger.install({ URL: xpiURL });
+ },
+ {
+ source: "unknown",
+ },
+ /* expectBlockedOrigin */ true
+ );
+});
+
+add_task(function teardown_clearUnexamitedTelemetry() {
+ // Clear collected telemetry events when we are not going to run any assertion on them.
+ // (otherwise the test will fail because of unexamined telemetry events).
+ AddonTestUtils.getAMTelemetryEvents();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_local_install.js b/toolkit/mozapps/extensions/test/browser/browser_local_install.js
new file mode 100644
index 0000000000..5200b69e39
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_local_install.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const XPI_INCOMPATIBLE_ID = "incompatible-xpi@tests.mozilla.org";
+// NOTE: we are using an HTTP url on purpose here, the test case fails
+// otherwise... We disable `AddonManager.checkUpdateSecurity` to allow
+// retrieving updates from HTTP (which is restored in a
+// `registerCleanupFunction()` or at the end of the task).
+//
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const BASE_URL = "http://fake-updates.example.com";
+
+const server = AddonTestUtils.createHttpServer({
+ hosts: ["fake-updates.example.com"],
+});
+
+const UPDATE_ENTRY_COMPATIBLE = {
+ // NOTE: this version must be the exact same one associated than the
+ // initially incompatible XPI, otherwise it won't override the initial
+ // compatibility range.
+ // See the check in `AddonUpdateChecker.getCompatibilityUpdate` here:
+ // https://searchfox.org/mozilla-central/rev/4044c340/toolkit/mozapps/extensions/internal/AddonUpdateChecker.sys.mjs#489
+ version: "4.0",
+ // An empty compatibility range will make this update to be overriding the
+ // incompatible range in the xpi and makes the xpi version to be considered
+ // compatible.
+ applications: { gecko: {} },
+};
+
+const UPDATE_ENTRY_INCOMPATIBLE = {
+ ...UPDATE_ENTRY_COMPATIBLE,
+ // This update entry instead is including a compatibility range that would
+ // makes the xpi version being installed to be considered still incompatible.
+ applications: {
+ gecko: {
+ strict_min_version: "41",
+ strict_max_version: "41.*",
+ },
+ },
+};
+
+AddonTestUtils.registerJSON(server, "/updates-still-incompatible.json", {
+ addons: {
+ [XPI_INCOMPATIBLE_ID]: {
+ updates: [UPDATE_ENTRY_INCOMPATIBLE],
+ },
+ },
+});
+
+AddonTestUtils.registerJSON(server, "/updates-now-compatible.json", {
+ addons: {
+ [XPI_INCOMPATIBLE_ID]: {
+ updates: [UPDATE_ENTRY_COMPATIBLE],
+ },
+ },
+});
+
+add_task(async function test_local_install_blocklisted() {
+ let id = "amosigned-xpi@tests.mozilla.org";
+ let version = "2.1";
+
+ await AddonTestUtils.loadBlocklistRawData({
+ extensionsMLBF: [
+ {
+ stash: { blocked: [`${id}:${version}`], unblocked: [] },
+ stash_time: 0,
+ },
+ ],
+ });
+ let needsCleanupBlocklist = true;
+ const cleanupBlocklist = async () => {
+ if (!needsCleanupBlocklist) {
+ return;
+ }
+ await AddonTestUtils.loadBlocklistRawData({
+ extensionsMLBF: [
+ {
+ stash: { blocked: [], unblocked: [] },
+ stash_time: 0,
+ },
+ ],
+ });
+ needsCleanupBlocklist = false;
+ };
+ registerCleanupFunction(cleanupBlocklist);
+
+ const xpiFilePath = getTestFilePath("../xpinstall/amosigned.xpi");
+ const xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ xpiFile.initWithPath(xpiFilePath);
+ ok(xpiFile.exists(), "Expect the xpi file to exist");
+ const xpiFileURI = Services.io.newFileURI(xpiFile);
+
+ let install = await AddonManager.getInstallForURL(xpiFileURI.spec, {
+ telemetryInfo: { source: "file-url" },
+ });
+ const promiseInstallFailed = BrowserUtils.promiseObserved(
+ "addon-install-failed",
+ subject => {
+ return subject.wrappedJSObject.installs[0] == install;
+ }
+ );
+
+ AddonManager.installAddonFromWebpage(
+ "application/x-xpinstall",
+ gBrowser.selectedBrowser,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ install
+ );
+
+ info("Wait for addon-install-failed to be notified");
+ await promiseInstallFailed;
+ Assert.equal(
+ install.error,
+ AddonManager.ERROR_BLOCKLISTED,
+ "LocalInstall cancelled with the expected error"
+ );
+
+ await cleanupBlocklist();
+});
+
+add_task(async function test_local_install_incompatible() {
+ const xpiFilePath = getTestFilePath("../xpinstall/incompatible.xpi");
+ const xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ xpiFile.initWithPath(xpiFilePath);
+ ok(xpiFile.exists(), "Expect the xpi file to exist");
+ const xpiFileURI = Services.io.newFileURI(xpiFile);
+
+ const installTestExtension = async ({ expectIncompatible }) => {
+ let install = await AddonManager.getInstallForURL(xpiFileURI.spec, {
+ telemetryInfo: { source: "file-url" },
+ });
+ const promiseInstallDone = expectIncompatible
+ ? BrowserUtils.promiseObserved(
+ "addon-install-failed",
+ subject => subject.wrappedJSObject.installs[0] == install
+ )
+ : BrowserUtils.promiseObserved(
+ "webextension-permission-prompt",
+ subject => subject.wrappedJSObject.info.addon == install.addon
+ );
+
+ AddonManager.installAddonFromWebpage(
+ "application/x-xpinstall",
+ gBrowser.selectedBrowser,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ install
+ );
+
+ if (expectIncompatible) {
+ info("Wait for addon-install-failed to be notified");
+ await promiseInstallDone;
+ Assert.equal(
+ install.error,
+ AddonManager.ERROR_INCOMPATIBLE,
+ "LocalInstall cancelled with the expected error"
+ );
+ } else {
+ info("Wait for webextension-permission-prompt to be notified");
+ await promiseInstallDone;
+ Assert.equal(
+ install.error,
+ 0,
+ "no error expected on the LocalInstall instance"
+ );
+ Assert.equal(
+ install.state,
+ AddonManager.STATE_DOWNLOADED,
+ "Got the expected LocalInstall state"
+ );
+ Assert.ok(
+ install.addon.isCompatible,
+ "updated Addon XPI is expected to be compatible"
+ );
+ Assert.equal(
+ install.addon.version,
+ "4.0",
+ "Addon version expected to match the updated xpi file"
+ );
+ // Cancel the installation, before exiting the test.
+ await install.cancel();
+ }
+ };
+
+ info("Test incompatible xpi without a compatibility override");
+ // Use a new tab to make sure the doorhanger will be gone when
+ // the test tab is being removed (same when repeating the
+ // test with expectIncompatible set to false).
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await installTestExtension({ expectIncompatible: true });
+ });
+
+ // Add the prefs to ignore signature checks for this test (allowed on all
+ // channels while running in automation).
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.update.url", `${BASE_URL}/updates.json`],
+ ["xpinstall.signatures.required", false],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+ AddonManager.checkUpdateSecurity = false;
+ registerCleanupFunction(() => {
+ AddonManager.checkUpdateSecurity = true;
+ });
+
+ info(
+ "Test incompatible xpi with a compatibility override that is still incompatible"
+ );
+ // Add the prefs to provide a compatibility range override which is still
+ // incompatible.
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.update.url", `${BASE_URL}/updates-still-incompatible.json`],
+ ],
+ });
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await installTestExtension({ expectIncompatible: true });
+ });
+ SpecialPowers.popPrefEnv();
+
+ info(
+ "Test incompatible xpi with a compatibility override that makes it compatible"
+ );
+ // Add the prefs to provide a compatibility range override which is
+ // compatible.
+ SpecialPowers.pushPrefEnv({
+ set: [["extensions.update.url", `${BASE_URL}/updates-now-compatible.json`]],
+ });
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await installTestExtension({ expectIncompatible: false });
+ });
+ SpecialPowers.popPrefEnv();
+
+ SpecialPowers.popPrefEnv();
+ AddonManager.checkUpdateSecurity = true;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js
new file mode 100644
index 0000000000..aee47dd049
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js
@@ -0,0 +1,331 @@
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+
+function extensionShortcutsReady(id) {
+ let extension = WebExtensionPolicy.getByID(id).extension;
+ return BrowserTestUtils.waitForCondition(() => {
+ return extension.shortcuts.keysetsMap.has(window);
+ }, "Wait for add-on keyset to be registered");
+}
+
+async function loadShortcutsView() {
+ // Load the theme view initially so we can verify that the category is switched
+ // to "extension" when the shortcuts view is loaded.
+ let win = await loadInitialView("theme");
+ let categoryUtils = new CategoryUtilities(win);
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/theme",
+ "The theme category is selected"
+ );
+
+ let shortcutsLink = win.document.querySelector(
+ '#page-options [action="manage-shortcuts"]'
+ );
+ ok(!shortcutsLink.hidden, "The shortcuts link is visible");
+
+ let loaded = waitForViewLoad(win);
+ shortcutsLink.click();
+ await loaded;
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/extension",
+ "The extension category is now selected"
+ );
+
+ return win;
+}
+
+add_task(async function testUpdatingCommands() {
+ let commands = {
+ commandZero: {},
+ commandOne: {
+ suggested_key: { default: "Shift+Alt+7" },
+ },
+ commandTwo: {
+ description: "Command Two!",
+ suggested_key: { default: "Alt+4" },
+ },
+ _execute_browser_action: {
+ suggested_key: { default: "Shift+Alt+9" },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands,
+ browser_action: { default_popup: "popup.html" },
+ },
+ background() {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.sendMessage("oncommand", commandName);
+ });
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extensionShortcutsReady(extension.id);
+
+ async function checkShortcut(name, key, modifiers) {
+ EventUtils.synthesizeKey(key, modifiers);
+ let message = await extension.awaitMessage("oncommand");
+ is(
+ message,
+ name,
+ `Expected onCommand listener to fire with the correct name: ${name}`
+ );
+ }
+
+ // Load the about:addons shortcut view before verify that emitting
+ // the key events does trigger the expected extension commands.
+ // There is apparently a race (more likely to be triggered on an
+ // optimized build) between:
+ // - the new opened browser window to be ready to listen for the
+ // keyboard events that are expected to triggered one of the key
+ // in the extension keyset
+ // - and the test calling EventUtils.syntesizeKey to test that
+ // the expected extension command listener is notified.
+ //
+ // Loading the shortcut view before calling checkShortcut seems to be
+ // enough to consistently avoid that race condition.
+ let win = await loadShortcutsView();
+
+ // Check that the original shortcuts work.
+ await checkShortcut("commandOne", "7", { shiftKey: true, altKey: true });
+ await checkShortcut("commandTwo", "4", { altKey: true });
+
+ let doc = win.document;
+
+ let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
+ ok(card, `There is a card for the extension`);
+
+ let inputs = card.querySelectorAll(".shortcut-input");
+ is(
+ inputs.length,
+ Object.keys(commands).length,
+ "There is an input for each command"
+ );
+
+ let nameOrder = Array.from(inputs).map(input => input.getAttribute("name"));
+ Assert.deepEqual(
+ nameOrder,
+ ["commandOne", "commandTwo", "_execute_browser_action", "commandZero"],
+ "commandZero should be last since it is unset"
+ );
+
+ let count = 1;
+ for (let input of inputs) {
+ // Change the shortcut.
+ input.focus();
+ EventUtils.synthesizeKey("8", { shiftKey: true, altKey: true });
+ count++;
+
+ // Wait for the shortcut attribute to change.
+ await BrowserTestUtils.waitForCondition(
+ () => input.getAttribute("shortcut") == "Alt+Shift+8",
+ "Wait for shortcut to update to Alt+Shift+8"
+ );
+
+ // Check that the change worked (but skip if browserAction).
+ if (input.getAttribute("name") != "_execute_browser_action") {
+ await checkShortcut(input.getAttribute("name"), "8", {
+ shiftKey: true,
+ altKey: true,
+ });
+ }
+
+ // Change it again so it doesn't conflict with the next command.
+ input.focus();
+ EventUtils.synthesizeKey(count.toString(), {
+ shiftKey: true,
+ altKey: true,
+ });
+ await BrowserTestUtils.waitForCondition(
+ () => input.getAttribute("shortcut") == `Alt+Shift+${count}`,
+ `Wait for shortcut to update to Alt+Shift+${count}`
+ );
+ }
+
+ // Check that errors can be shown.
+ let input = inputs[0];
+ let error = doc.querySelector(".error-message");
+ let label = error.querySelector(".error-message-label");
+ is(error.style.visibility, "hidden", "The error is initially hidden");
+
+ // Try a shortcut with only shift for a modifier.
+ input.focus();
+ EventUtils.synthesizeKey("J", { shiftKey: true });
+ let possibleErrors = ["shortcuts-modifier-mac", "shortcuts-modifier-other"];
+ ok(possibleErrors.includes(label.dataset.l10nId), `The message is set`);
+ is(error.style.visibility, "visible", "The error is shown");
+
+ // Escape should clear the focus and hide the error.
+ is(doc.activeElement, input, "The input is focused");
+ EventUtils.synthesizeKey("Escape", {});
+ Assert.notEqual(doc.activeElement, input, "The input is no longer focused");
+ is(error.style.visibility, "hidden", "The error is hidden");
+
+ // Check if assigning already assigned shortcut is prevented.
+ input.focus();
+ EventUtils.synthesizeKey("2", { shiftKey: true, altKey: true });
+ is(label.dataset.l10nId, "shortcuts-exists", `The message is set`);
+ is(error.style.visibility, "visible", "The error is shown");
+
+ // Check the label uses the description first, and has a default for the special cases.
+ function checkLabel(name, value) {
+ let input = doc.querySelector(`.shortcut-input[name="${name}"]`);
+ let label = input.previousElementSibling;
+ if (label.dataset.l10nId) {
+ is(label.dataset.l10nId, value, "The l10n-id is set");
+ } else {
+ is(label.textContent, value, "The textContent is set");
+ }
+ }
+ checkLabel("commandOne", "commandOne");
+ checkLabel("commandTwo", "Command Two!");
+ checkLabel("_execute_browser_action", "shortcuts-browserAction2");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+async function startExtensionWithCommands(numCommands) {
+ let commands = {};
+
+ for (let i = 0; i < numCommands; i++) {
+ commands[`command-${i}`] = {};
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands,
+ },
+ background() {
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extensionShortcutsReady(extension.id);
+
+ return extension;
+}
+
+add_task(async function testExpanding() {
+ const numCommands = 7;
+ const visibleCommands = 5;
+
+ let extension = await startExtensionWithCommands(numCommands);
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
+ ok(!card.hasAttribute("expanded"), "The card is not expanded");
+
+ let shortcutRows = card.querySelectorAll(".shortcut-row");
+ is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`);
+
+ function assertCollapsedVisibility() {
+ for (let i = 0; i < shortcutRows.length; i++) {
+ let row = shortcutRows[i];
+ if (i < visibleCommands) {
+ Assert.notEqual(
+ getComputedStyle(row).display,
+ "none",
+ `The first ${visibleCommands} rows are visible`
+ );
+ } else {
+ is(getComputedStyle(row).display, "none", "The other rows are hidden");
+ }
+ }
+ }
+
+ // Check the visibility of the rows.
+ assertCollapsedVisibility();
+
+ let expandButton = card.querySelector(".expand-button");
+ ok(expandButton, "There is an expand button");
+ let l10nAttrs = doc.l10n.getAttributes(expandButton);
+ is(l10nAttrs.id, "shortcuts-card-expand-button", "The expand text is shown");
+ is(
+ l10nAttrs.args.numberToShow,
+ numCommands - visibleCommands,
+ "The number to be shown is set on the expand button"
+ );
+
+ // Expand the card.
+ expandButton.click();
+
+ is(card.getAttribute("expanded"), "true", "The card is now expanded");
+
+ for (let row of shortcutRows) {
+ Assert.notEqual(
+ getComputedStyle(row).display,
+ "none",
+ "All the rows are visible"
+ );
+ }
+
+ // The collapse text is now shown.
+ l10nAttrs = doc.l10n.getAttributes(expandButton);
+ is(
+ l10nAttrs.id,
+ "shortcuts-card-collapse-button",
+ "The colapse text is shown"
+ );
+
+ // Collapse the card.
+ expandButton.click();
+
+ ok(!card.hasAttribute("expanded"), "The card is now collapsed again");
+
+ assertCollapsedVisibility({ collapsed: true });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testOneExtraCommandIsNotCollapsed() {
+ const numCommands = 6;
+ let extension = await startExtensionWithCommands(numCommands);
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ // The card is not expanded, since it doesn't collapse.
+ let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
+ ok(!card.hasAttribute("expanded"), "The card is not expanded");
+
+ // Each shortcut has a row.
+ let shortcutRows = card.querySelectorAll(".shortcut-row");
+ is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`);
+
+ // There's no expand button, since it can't be expanded.
+ let expandButton = card.querySelector(".expand-button");
+ ok(!expandButton, "There is no expand button");
+
+ // All of the rows are visible, to avoid a "Show 1 More" button.
+ for (let row of shortcutRows) {
+ Assert.notEqual(
+ getComputedStyle(row).display,
+ "none",
+ "All the rows are visible"
+ );
+ }
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js
new file mode 100644
index 0000000000..327a99af9e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js
@@ -0,0 +1,198 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+async function loadShortcutsView() {
+ let managerWin = await open_manager(null);
+ managerWin.gViewController.loadView("addons://shortcuts/shortcuts");
+ await wait_for_view_load(managerWin);
+ return managerWin.document;
+}
+
+async function closeShortcutsView(doc) {
+ let managerWin = doc.defaultView.parent;
+ await close_manager(managerWin);
+}
+
+async function registerAndStartExtension(mockProvider, ext) {
+ // Shortcuts are registered when an extension is started, so we need to load
+ // and start an extension.
+ let extension = ExtensionTestUtils.loadExtension(ext);
+ await extension.startup();
+
+ // Extensions only appear in the add-on manager when they are registered with
+ // the add-on manager, e.g. by passing "useAddonManager" to `loadExtension`.
+ // "useAddonManager" can however not be used, because the resulting add-ons
+ // are unsigned, and only add-ons with privileged signatures can be hidden.
+ mockProvider.createAddons([
+ {
+ id: extension.id,
+ name: ext.manifest.name,
+ type: "extension",
+ version: "1",
+ // We use MockProvider because the "hidden" property cannot
+ // be set when "useAddonManager" is passed to loadExtension.
+ hidden: ext.manifest.hidden,
+ isSystem: ext.isSystem,
+ },
+ ]);
+ return extension;
+}
+
+function getShortcutCard(doc, extension) {
+ return doc.querySelector(`.shortcut[addon-id="${extension.id}"]`);
+}
+
+function getShortcutByName(doc, extension, name) {
+ let card = getShortcutCard(doc, extension);
+ return card && card.querySelector(`.shortcut-input[name="${name}"]`);
+}
+
+function getNoShortcutListItem(doc, extension) {
+ let { id } = extension;
+ let li = doc.querySelector(`.shortcuts-no-commands-list [addon-id="${id}"]`);
+ return li && li.textContent;
+}
+
+add_task(async function extension_with_shortcuts() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "shortcut addon",
+ commands: {
+ theShortcut: {},
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let doc = await loadShortcutsView();
+
+ ok(
+ getShortcutByName(doc, extension, "theShortcut"),
+ "Extension with shortcuts should have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, extension),
+ null,
+ "Extension with shortcuts should not be listed"
+ );
+
+ await closeShortcutsView(doc);
+ await extension.unload();
+});
+
+add_task(async function extension_without_shortcuts() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "no shortcut addon",
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let doc = await loadShortcutsView();
+
+ is(
+ getShortcutCard(doc, extension),
+ null,
+ "Extension without shortcuts should not have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, extension),
+ "no shortcut addon",
+ "The add-on's name is set in the list"
+ );
+
+ await closeShortcutsView(doc);
+ await extension.unload();
+});
+
+// Hidden add-ons without shortcuts should be hidden,
+// but their card should be shown if there is a shortcut.
+add_task(async function hidden_extension() {
+ let mockProvider = new MockProvider();
+ let hiddenExt1 = await registerAndStartExtension(mockProvider, {
+ manifest: {
+ name: "hidden with shortcuts",
+ hidden: true,
+ commands: {
+ hiddenShortcut: {},
+ },
+ },
+ });
+ let hiddenExt2 = await registerAndStartExtension(mockProvider, {
+ manifest: {
+ name: "hidden without shortcuts",
+ hidden: true,
+ },
+ });
+
+ let doc = await loadShortcutsView();
+
+ ok(
+ getShortcutByName(doc, hiddenExt1, "hiddenShortcut"),
+ "Hidden extension with shortcuts should have a card"
+ );
+
+ is(
+ getShortcutCard(doc, hiddenExt2),
+ null,
+ "Hidden extension without shortcuts should not have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, hiddenExt2),
+ null,
+ "Hidden extension without shortcuts should not be listed"
+ );
+
+ await closeShortcutsView(doc);
+ await hiddenExt1.unload();
+ await hiddenExt2.unload();
+
+ mockProvider.unregister();
+});
+
+add_task(async function system_addons_and_shortcuts() {
+ let mockProvider = new MockProvider();
+ let systemExt1 = await registerAndStartExtension(mockProvider, {
+ isSystem: true,
+ manifest: {
+ name: "system with shortcuts",
+ // In practice, all XPIStateLocations with isSystem=true also have
+ // isBuiltin=true, which implies that hidden=true as well.
+ hidden: true,
+ commands: {
+ systemShortcut: {},
+ },
+ },
+ });
+ let systemExt2 = await registerAndStartExtension(mockProvider, {
+ isSystem: true,
+ manifest: {
+ name: "system without shortcuts",
+ hidden: true,
+ },
+ });
+
+ let doc = await loadShortcutsView();
+
+ ok(
+ getShortcutByName(doc, systemExt1, "systemShortcut"),
+ "System add-on with shortcut should have a card"
+ );
+
+ is(
+ getShortcutCard(doc, systemExt2),
+ null,
+ "System add-on without shortcut should not have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, systemExt2),
+ null,
+ "System add-on without shortcuts should not be listed"
+ );
+
+ await closeShortcutsView(doc);
+ await systemExt1.unload();
+ await systemExt2.unload();
+
+ mockProvider.unregister();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js
new file mode 100644
index 0000000000..259c10d730
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js
@@ -0,0 +1,180 @@
+"use strict";
+
+async function loadShortcutsView() {
+ let managerWin = await open_manager(null);
+ managerWin.gViewController.loadView("addons://shortcuts/shortcuts");
+ await wait_for_view_load(managerWin);
+ return managerWin.document;
+}
+
+async function closeShortcutsView(doc) {
+ let managerWin = doc.defaultView.parent;
+ await close_manager(managerWin);
+}
+
+function getShortcutCard(doc, extension) {
+ return doc.querySelector(`.shortcut[addon-id="${extension.id}"]`);
+}
+
+function getShortcutByName(doc, extension, name) {
+ let card = getShortcutCard(doc, extension);
+ return card && card.querySelector(`.shortcut-input[name="${name}"]`);
+}
+
+async function waitForShortcutSet(input, expected) {
+ let doc = input.ownerDocument;
+ await BrowserTestUtils.waitForCondition(
+ () => input.getAttribute("shortcut") == expected,
+ `Shortcut should be set to ${JSON.stringify(expected)}`
+ );
+ Assert.notEqual(doc.activeElement, input, "The input is no longer focused");
+ checkHasRemoveButton(input, expected !== "");
+}
+
+function removeButtonForInput(input) {
+ let removeButton = input.parentNode.querySelector(".shortcut-remove-button");
+ ok(removeButton, "has remove button");
+ ok(
+ removeButton.hasAttribute("aria-label"),
+ "The remove button has an accessible name"
+ );
+ return removeButton;
+}
+
+function checkHasRemoveButton(input, expected) {
+ let removeButton = removeButtonForInput(input);
+ let visibility = input.ownerGlobal.getComputedStyle(removeButton).visibility;
+ if (expected) {
+ is(visibility, "visible", "Remove button should be visible");
+ } else {
+ is(visibility, "hidden", "Remove button should be hidden");
+ }
+}
+
+add_task(async function test_remove_shortcut() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ commandEmpty: {},
+ commandOne: {
+ suggested_key: { default: "Shift+Alt+1" },
+ },
+ commandTwo: {
+ suggested_key: { default: "Shift+Alt+2" },
+ },
+ },
+ },
+ background() {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.sendMessage("oncommand", commandName);
+ });
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ let doc = await loadShortcutsView();
+
+ let input = getShortcutByName(doc, extension, "commandOne");
+
+ checkHasRemoveButton(input, true);
+
+ // First: Verify that Shift-Del is not valid, but doesn't do anything.
+ input.focus();
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ let errorElem = doc.querySelector(".error-message");
+ is(errorElem.style.visibility, "visible", "Expected error message");
+ let errorId = doc.l10n.getAttributes(
+ errorElem.querySelector(".error-message-label")
+ ).id;
+ if (AppConstants.platform == "macosx") {
+ is(errorId, "shortcuts-modifier-mac", "Shift-Del is not a valid shortcut");
+ } else {
+ is(errorId, "shortcuts-modifier-other", "Shift-Del isn't a valid shortcut");
+ }
+ checkHasRemoveButton(input, true);
+
+ // Now, verify that the original shortcut still works.
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.notEqual(doc.activeElement, input, "The input is no longer focused");
+ is(errorElem.style.visibility, "hidden", "The error is hidden");
+
+ EventUtils.synthesizeKey("1", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("oncommand");
+
+ // Alt-Shift-Del is a valid shortcut.
+ input.focus();
+ EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true });
+ await waitForShortcutSet(input, "Alt+Shift+Delete");
+ EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("oncommand");
+
+ // Del without modifiers should clear the shortcut.
+ input.focus();
+ EventUtils.synthesizeKey("KEY_Delete");
+ await waitForShortcutSet(input, "");
+ // Trigger the shortcuts that were originally associated with commandOne,
+ // and then trigger commandTwo. The extension should only see commandTwo.
+ EventUtils.synthesizeKey("1", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true });
+ is(
+ await extension.awaitMessage("oncommand"),
+ "commandTwo",
+ "commandOne should be disabled, commandTwo should still be enabled"
+ );
+
+ // Set a shortcut where the default was not set.
+ let inputEmpty = getShortcutByName(doc, extension, "commandEmpty");
+ is(inputEmpty.getAttribute("shortcut"), "", "Empty shortcut by default");
+ checkHasRemoveButton(input, false);
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ await waitForShortcutSet(inputEmpty, "Alt+Shift+3");
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("oncommand");
+ // Clear shortcut.
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("KEY_Delete");
+ await waitForShortcutSet(inputEmpty, "");
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true });
+ is(
+ await extension.awaitMessage("oncommand"),
+ "commandTwo",
+ "commandEmpty should be disabled, commandTwo should still be enabled"
+ );
+
+ // Now verify that the Backspace button does the same thing as Delete.
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ await waitForShortcutSet(inputEmpty, "Alt+Shift+3");
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await waitForShortcutSet(input, "");
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true });
+ is(
+ await extension.awaitMessage("oncommand"),
+ "commandTwo",
+ "commandEmpty should be disabled again by Backspace"
+ );
+
+ // Check that the remove button works as expected.
+ let inputTwo = getShortcutByName(doc, extension, "commandTwo");
+ is(inputTwo.getAttribute("shortcut"), "Shift+Alt+2", "initial shortcut");
+ checkHasRemoveButton(inputTwo, true);
+ removeButtonForInput(inputTwo).click();
+ is(inputTwo.getAttribute("shortcut"), "", "cleared shortcut");
+ checkHasRemoveButton(inputTwo, false);
+ Assert.notEqual(
+ doc.activeElement,
+ inputTwo,
+ "input of removed shortcut is not focused"
+ );
+
+ await closeShortcutsView(doc);
+
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js
new file mode 100644
index 0000000000..a602d84999
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function testOpenMenu(btn, method) {
+ let shown = BrowserTestUtils.waitForEvent(btn.ownerGlobal, "shown", true);
+ await method();
+ await shown;
+ is(btn.getAttribute("aria-expanded"), "true", "expanded when open");
+}
+
+async function testCloseMenu(btn, method) {
+ let hidden = BrowserTestUtils.waitForEvent(btn.ownerGlobal, "hidden", true);
+ await method();
+ await hidden;
+ is(btn.getAttribute("aria-expanded"), "false", "not expanded when closed");
+}
+
+async function testButton(btn) {
+ let win = btn.ownerGlobal;
+
+ is(btn.getAttribute("aria-haspopup"), "menu", "it has a menu");
+ is(btn.getAttribute("aria-expanded"), "false", "not expanded");
+
+ info("Test open/close with mouse");
+ await testOpenMenu(btn, () => {
+ EventUtils.synthesizeMouseAtCenter(btn, {}, win);
+ });
+ await testCloseMenu(btn, () => {
+ let spacer = win.document.querySelector(".main-heading .spacer");
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive element to dismiss the
+ // opened menu with a mouse which can be done by assistive technology and
+ // keyboard by pressing `Esc` key, this rule check shall be ignored by
+ // a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ EventUtils.synthesizeMouseAtCenter(spacer, {}, win);
+ AccessibilityUtils.resetEnv();
+ });
+
+ info("Test open/close with keyboard");
+ await testOpenMenu(btn, async () => {
+ btn.focus();
+ EventUtils.synthesizeKey(" ", {}, win);
+ });
+ await testCloseMenu(btn, () => {
+ EventUtils.synthesizeKey("Escape", {}, win);
+ });
+}
+
+add_task(async function testPageOptionsMenuButton() {
+ let win = await loadInitialView("extension");
+
+ await testButton(
+ win.document.querySelector(".page-options-menu .more-options-button")
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testCardMoreOptionsButton() {
+ let id = "more-options-button@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let card = getAddonCard(win, id);
+
+ info("Check list page");
+ await testButton(card.querySelector(".more-options-button"));
+
+ let viewLoaded = waitForViewLoad(win);
+
+ EventUtils.synthesizeMouseAtCenter(
+ card.querySelector(".addon-name-link"),
+ {},
+ win
+ );
+ await viewLoaded;
+
+ info("Check detail page");
+ card = getAddonCard(win, id);
+ await testButton(card.querySelector(".more-options-button"));
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js
new file mode 100644
index 0000000000..e049cbd618
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPageTitle() {
+ let win = await loadInitialView("extension");
+ let title = win.document.querySelector("title");
+ is(
+ win.document.l10n.getAttributes(title).id,
+ "addons-page-title",
+ "The page title is set"
+ );
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js
new file mode 100644
index 0000000000..5007731927
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests bug 567127 - Add install button to the add-ons manager
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+async function checkInstallConfirmation(...names) {
+ let notificationCount = 0;
+ let observer = {
+ observe(aSubject, aTopic, aData) {
+ var installInfo = aSubject.wrappedJSObject;
+ isnot(
+ installInfo.browser,
+ null,
+ "Notification should have non-null browser"
+ );
+ Assert.deepEqual(
+ installInfo.installs[0].installTelemetryInfo,
+ {
+ source: "about:addons",
+ method: "install-from-file",
+ },
+ "Got the expected installTelemetryInfo"
+ );
+ notificationCount++;
+ },
+ };
+ Services.obs.addObserver(observer, "addon-install-started");
+
+ let results = [];
+
+ let promise = promisePopupNotificationShown("addon-webext-permissions");
+ for (let i = 0; i < names.length; i++) {
+ let panel = await promise;
+ let name = panel.getAttribute("name");
+ results.push(name);
+
+ info(`Saw install for ${name}`);
+ if (results.length < names.length) {
+ info(
+ `Waiting for installs for ${names.filter(n => !results.includes(n))}`
+ );
+
+ promise = promisePopupNotificationShown("addon-webext-permissions");
+ }
+ panel.secondaryButton.click();
+ }
+
+ Assert.deepEqual(results.sort(), names.sort(), "Got expected installs");
+
+ is(
+ notificationCount,
+ names.length,
+ `Saw ${names.length} addon-install-started notification`
+ );
+ Services.obs.removeObserver(observer, "addon-install-started");
+}
+
+add_task(async function test_install_from_file() {
+ let win = await loadInitialView("extension");
+
+ var filePaths = [
+ get_addon_file_url("browser_dragdrop1.xpi"),
+ get_addon_file_url("browser_dragdrop2.xpi"),
+ ];
+ for (let uri of filePaths) {
+ Assert.notEqual(uri.file, null, `Should have file for ${uri.spec}`);
+ ok(uri.file instanceof Ci.nsIFile, `Should have nsIFile for ${uri.spec}`);
+ }
+ MockFilePicker.setFiles(filePaths.map(aPath => aPath.file));
+
+ // Set handler that executes the core test after the window opens,
+ // and resolves the promise when the window closes
+ let pInstallURIClosed = checkInstallConfirmation(
+ "Drag Drop test 1",
+ "Drag Drop test 2"
+ );
+
+ win.document
+ .querySelector('#page-options [action="install-from-file"]')
+ .click();
+
+ await pInstallURIClosed;
+
+ MockFilePicker.cleanup();
+ await closeView(win);
+});
+
+add_task(async function test_install_disabled() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let pageOptionsMenu = doc.querySelector("addon-page-options panel-list");
+
+ function openPageOptions() {
+ let opened = BrowserTestUtils.waitForEvent(pageOptionsMenu, "shown");
+ pageOptionsMenu.open = true;
+ return opened;
+ }
+
+ function closePageOptions() {
+ let closed = BrowserTestUtils.waitForEvent(pageOptionsMenu, "hidden");
+ pageOptionsMenu.open = false;
+ return closed;
+ }
+
+ await openPageOptions();
+ let installButton = doc.querySelector('[action="install-from-file"]');
+ ok(!installButton.hidden, "The install button is shown");
+ await closePageOptions();
+
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_XPI_ENABLED, false]] });
+
+ await openPageOptions();
+ ok(installButton.hidden, "The install button is now hidden");
+ await closePageOptions();
+
+ await SpecialPowers.popPrefEnv();
+
+ await openPageOptions();
+ ok(!installButton.hidden, "The install button is shown again");
+ await closePageOptions();
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js
new file mode 100644
index 0000000000..bd7572a061
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Make sure we don't accidentally start a background update while the prefs
+// are enabled.
+disableBackgroundUpdateTimer();
+registerCleanupFunction(() => {
+ enableBackgroundUpdateTimer();
+});
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const PREF_UPDATE_ENABLED = "extensions.update.enabled";
+const PREF_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
+
+add_task(async function testUpdateAutomaticallyButton() {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_UPDATE_ENABLED, true],
+ [PREF_AUTOUPDATE_DEFAULT, true],
+ ],
+ });
+
+ let win = await loadInitialView("extension");
+
+ let toggleAutomaticButton = win.document.querySelector(
+ '#page-options [action="set-update-automatically"]'
+ );
+
+ info("Verify the checked state reflects the update state");
+ ok(toggleAutomaticButton.checked, "Automatic updates button is checked");
+
+ AddonManager.autoUpdateDefault = false;
+ ok(!toggleAutomaticButton.checked, "Automatic updates button is unchecked");
+
+ AddonManager.autoUpdateDefault = true;
+ ok(toggleAutomaticButton.checked, "Automatic updates button is re-checked");
+
+ info("Verify that clicking the button changes the update state");
+ ok(AddonManager.autoUpdateDefault, "Auto updates are default");
+ ok(AddonManager.updateEnabled, "Updates are enabled");
+
+ toggleAutomaticButton.click();
+ ok(!AddonManager.autoUpdateDefault, "Auto updates are disabled");
+ ok(AddonManager.updateEnabled, "Updates are enabled");
+
+ toggleAutomaticButton.click();
+ ok(AddonManager.autoUpdateDefault, "Auto updates are enabled again");
+ ok(AddonManager.updateEnabled, "Updates are enabled");
+
+ await closeView(win);
+});
+
+add_task(async function testResetUpdateStates() {
+ let id = "update-state@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let resetStateButton = win.document.querySelector(
+ '#page-options [action="reset-update-states"]'
+ );
+
+ info("Changing add-on update state");
+ let addon = await AddonManager.getAddonByID(id);
+
+ let setAddonUpdateState = async updateState => {
+ let changed = AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ addon.applyBackgroundUpdates = updateState;
+ await changed;
+ let addonState = addon.applyBackgroundUpdates;
+ is(addonState, updateState, `Add-on updates are ${updateState}`);
+ };
+
+ await setAddonUpdateState(AddonManager.AUTOUPDATE_DISABLE);
+
+ let propertyChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ resetStateButton.click();
+ await propertyChanged;
+ is(
+ addon.applyBackgroundUpdates,
+ AddonManager.AUTOUPDATE_DEFAULT,
+ "Add-on is reset to default updates"
+ );
+
+ await setAddonUpdateState(AddonManager.AUTOUPDATE_ENABLE);
+
+ propertyChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ resetStateButton.click();
+ await propertyChanged;
+ is(
+ addon.applyBackgroundUpdates,
+ AddonManager.AUTOUPDATE_DEFAULT,
+ "Add-on is reset to default updates again"
+ );
+
+ info("Check the label on the button as the global state changes");
+ is(
+ win.document.l10n.getAttributes(resetStateButton).id,
+ "addon-updates-reset-updates-to-automatic",
+ "The reset button label says it resets to automatic"
+ );
+
+ info("Disable auto updating globally");
+ AddonManager.autoUpdateDefault = false;
+
+ is(
+ win.document.l10n.getAttributes(resetStateButton).id,
+ "addon-updates-reset-updates-to-manual",
+ "The reset button label says it resets to manual"
+ );
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js b/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js
new file mode 100644
index 0000000000..d58eb8c027
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/*
+ * Test Permission Popup for Sideloaded Extensions.
+ */
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const ADDON_ID = "addon1@test.mozilla.org";
+const CUSTOM_THEME_ID = "theme1@test.mozilla.org";
+const DEFAULT_THEME_ID = "default-theme@mozilla.org";
+
+AddonTestUtils.initMochitest(this);
+
+function assertDisabledSideloadedExtensionElement(managerWindow, addonElement) {
+ const doc = addonElement.ownerDocument;
+ const toggleDisabled = addonElement.querySelector(
+ '[action="toggle-disabled"]'
+ );
+ is(
+ doc.l10n.getAttributes(toggleDisabled).id,
+ "extension-enable-addon-button-label",
+ "Addon toggle-disabled action has the enable label"
+ );
+ ok(!toggleDisabled.checked, "toggle-disable isn't checked");
+}
+
+function assertEnabledSideloadedExtensionElement(managerWindow, addonElement) {
+ const doc = addonElement.ownerDocument;
+ const toggleDisabled = addonElement.querySelector(
+ '[action="toggle-disabled"]'
+ );
+ is(
+ doc.l10n.getAttributes(toggleDisabled).id,
+ "extension-enable-addon-button-label",
+ "Addon toggle-disabled action has the enable label"
+ );
+ ok(!toggleDisabled.checked, "toggle-disable isn't checked");
+}
+
+function clickEnableExtension(addonElement) {
+ addonElement.querySelector('[action="toggle-disabled"]').click();
+}
+
+// Test for bug 1647931
+// Install a theme, enable it and then enable the default theme again
+add_task(async function test_theme_enable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+
+ let theme = {
+ manifest: {
+ browser_specific_settings: { gecko: { id: CUSTOM_THEME_ID } },
+ name: "Theme 1",
+ theme: {
+ colors: {
+ frame: "#000000",
+ tab_background_text: "#ffffff",
+ },
+ },
+ },
+ };
+
+ let xpi = AddonTestUtils.createTempWebExtensionFile(theme);
+ await AddonTestUtils.manuallyInstall(xpi);
+
+ let changePromise = new Promise(resolve =>
+ ExtensionsUI.once("change", resolve)
+ );
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // enable fresh installed theme
+ let manager = await open_manager("addons://list/theme");
+ let customTheme = getAddonCard(manager, CUSTOM_THEME_ID);
+ clickEnableExtension(customTheme);
+
+ // enable default theme again
+ let defaultTheme = getAddonCard(manager, DEFAULT_THEME_ID);
+ clickEnableExtension(defaultTheme);
+
+ let addon = await AddonManager.getAddonByID(CUSTOM_THEME_ID);
+ await close_manager(manager);
+ await addon.uninstall();
+});
+
+// Loading extension by sideloading method
+add_task(async function test_sideloaded_extension_permissions_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+
+ let options = {
+ manifest: {
+ browser_specific_settings: { gecko: { id: ADDON_ID } },
+ name: "Test 1",
+ permissions: ["history", "https://*/*"],
+ icons: { 64: "foo-icon.png" },
+ },
+ };
+
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+ await AddonTestUtils.manuallyInstall(xpi);
+
+ let changePromise = new Promise(resolve =>
+ ExtensionsUI.once("change", resolve)
+ );
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // Test click event on permission cancel option.
+ let manager = await open_manager("addons://list/extension");
+ let addon = getAddonCard(manager, ADDON_ID);
+
+ Assert.notEqual(addon, null, "Found sideloaded addon in about:addons");
+
+ assertDisabledSideloadedExtensionElement(manager, addon);
+
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ clickEnableExtension(addon);
+ let panel = await popupPromise;
+
+ ok(PopupNotifications.isPanelOpen, "Permission popup should be visible");
+ panel.secondaryButton.click();
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "Permission popup should be closed / closing"
+ );
+
+ addon = await AddonManager.getAddonByID(ADDON_ID);
+ ok(
+ !addon.seen,
+ "Seen flag should remain false after permissions are refused"
+ );
+
+ // Test click event on permission accept option.
+ addon = getAddonCard(manager, ADDON_ID);
+ Assert.notEqual(addon, null, "Found sideloaded addon in about:addons");
+
+ assertEnabledSideloadedExtensionElement(manager, addon);
+
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ clickEnableExtension(addon);
+ panel = await popupPromise;
+
+ ok(PopupNotifications.isPanelOpen, "Permission popup should be visible");
+
+ let notificationPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ ADDON_ID
+ );
+
+ panel.button.click();
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "Permission popup should be closed / closing"
+ );
+ await notificationPromise;
+
+ addon = await AddonManager.getAddonByID(ADDON_ID);
+ ok(addon.seen, "Seen flag should be true after permissions are accepted");
+
+ ok(!PopupNotifications.isPanelOpen, "Permission popup should not be visible");
+
+ await close_manager(manager);
+ await addon.uninstall();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_reinstall.js b/toolkit/mozapps/extensions/test/browser/browser_reinstall.js
new file mode 100644
index 0000000000..c0eb7d139a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_reinstall.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that upgrading bootstrapped add-ons behaves correctly while the
+// manager is open
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const ID = "reinstall@tests.mozilla.org";
+const testIdSuffix = "@tests.mozilla.org";
+
+let gManagerWindow, xpi1, xpi2;
+
+function htmlDoc() {
+ return gManagerWindow.document;
+}
+
+function get_list_item_count() {
+ return htmlDoc().querySelectorAll(`addon-card[addon-id$="${testIdSuffix}"]`)
+ .length;
+}
+
+function removeItem(item) {
+ let button = item.querySelector('[action="remove"]');
+ button.click();
+}
+
+function hasPendingMessage(item, msg) {
+ let messageBar = htmlDoc().querySelector(
+ `moz-message-bar[addon-id="${item.addon.id}"`
+ );
+ is_element_visible(messageBar, msg);
+}
+
+async function install_addon(xpi) {
+ let install = await AddonManager.getInstallForFile(
+ xpi,
+ "application/x-xpinstall"
+ );
+ return install.install();
+}
+
+async function check_addon(aAddon, aVersion) {
+ is(get_list_item_count(), 1, "Should be one item in the list");
+ is(aAddon.version, aVersion, "Add-on should have the right version");
+
+ let item = getAddonCard(gManagerWindow, ID);
+ ok(!!item, "Should see the add-on in the list");
+
+ // Force XBL to apply
+ item.clientTop;
+
+ let { version } = await get_tooltip_info(item, gManagerWindow);
+ is(version, aVersion, "Version should be correct");
+
+ const l10nAttrs = item.ownerDocument.l10n.getAttributes(item.addonNameEl);
+ if (aAddon.userDisabled) {
+ Assert.deepEqual(
+ l10nAttrs,
+ { id: "addon-name-disabled", args: { name: aAddon.name } },
+ "localized addon name is marked as disabled"
+ );
+ } else {
+ Assert.deepEqual(
+ l10nAttrs,
+ { id: null, args: null },
+ "localized addon name is not marked as disabled"
+ );
+ }
+}
+
+async function wait_for_addon_item_added(addonId) {
+ await BrowserTestUtils.waitForEvent(
+ htmlDoc().querySelector("addon-list"),
+ "add"
+ );
+ const item = getAddonCard(gManagerWindow, addonId);
+ ok(item, `Found addon card for ${addonId}`);
+}
+
+async function wait_for_addon_item_removed(addonId) {
+ await BrowserTestUtils.waitForEvent(
+ htmlDoc().querySelector("addon-list"),
+ "remove"
+ );
+ const item = getAddonCard(gManagerWindow, addonId);
+ ok(!item, `There shouldn't be an addon card for ${addonId}`);
+}
+
+function wait_for_addon_item_updated(addonId) {
+ return BrowserTestUtils.waitForEvent(
+ getAddonCard(gManagerWindow, addonId),
+ "update"
+ );
+}
+
+// Install version 1 then upgrade to version 2 with the manager open
+async function test_upgrade_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let addon = await promiseAddonByID(ID);
+ await check_addon(addon, "1.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ await install_addon(xpi2);
+ await promiseItemUpdated;
+
+ addon = await promiseAddonByID(ID);
+ await check_addon(addon, "2.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+// Install version 1 mark it as disabled then upgrade to version 2 with the
+// manager open
+async function test_upgrade_disabled_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ let addon = await promiseAddonByID(ID);
+ await addon.disable();
+ await promiseItemUpdated;
+
+ await check_addon(addon, "1.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ promiseItemUpdated = wait_for_addon_item_updated(ID);
+ await install_addon(xpi2);
+ await promiseItemUpdated;
+
+ addon = await promiseAddonByID(ID);
+ await check_addon(addon, "2.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+// Install version 1 click the remove button and then upgrade to version 2 with
+// the manager open
+async function test_upgrade_pending_uninstall_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let addon = await promiseAddonByID(ID);
+ await check_addon(addon, "1.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ let item = getAddonCard(gManagerWindow, ID);
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ removeItem(item);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ await promiseItemRemoved;
+
+ ok(
+ !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "Add-on should be pending uninstall"
+ );
+ hasPendingMessage(item, "Pending message should be visible");
+
+ promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi2);
+ await promiseItemAdded;
+
+ addon = await promiseAddonByID(ID);
+ await check_addon(addon, "2.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+// Install version 1, disable it, click the remove button and then upgrade to
+// version 2 with the manager open
+async function test_upgrade_pending_uninstall_disabled_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ let addon = await promiseAddonByID(ID);
+ await addon.disable();
+ await promiseItemUpdated;
+
+ await check_addon(addon, "1.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ let item = getAddonCard(gManagerWindow, ID);
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ removeItem(item);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ await promiseItemRemoved;
+ ok(
+ !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "Add-on should be pending uninstall"
+ );
+ hasPendingMessage(item, "Pending message should be visible");
+
+ promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi2);
+ addon = await promiseAddonByID(ID);
+
+ await promiseItemAdded;
+ await check_addon(addon, "2.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+add_setup(async function () {
+ xpi1 = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: ID } },
+ },
+ });
+
+ xpi2 = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ browser_specific_settings: { gecko: { id: ID } },
+ },
+ });
+
+ // Accept all prompts.
+ mockPromptService()._response = 0;
+});
+
+add_task(async function test_upgrades() {
+ // Close existing about:addons tab if a test failure has
+ // prevented it from being closed.
+ if (gManagerWindow) {
+ await close_manager(gManagerWindow);
+ }
+
+ gManagerWindow = await open_manager("addons://list/extension");
+
+ await test_upgrade_v1_to_v2();
+ await test_upgrade_disabled_v1_to_v2();
+ await test_upgrade_pending_uninstall_v1_to_v2();
+ await test_upgrade_pending_uninstall_disabled_v1_to_v2();
+
+ await close_manager(gManagerWindow);
+ gManagerWindow = null;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js
new file mode 100644
index 0000000000..912ce8d62f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js
@@ -0,0 +1,262 @@
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+
+async function loadShortcutsView() {
+ let win = await loadInitialView("extension");
+
+ // There should be a manage shortcuts link.
+ let shortcutsLink = win.document.querySelector('[action="manage-shortcuts"]');
+
+ // Open the shortcuts view.
+ let loaded = waitForViewLoad(win);
+ shortcutsLink.click();
+ await loaded;
+
+ return win;
+}
+
+add_task(async function testDuplicateShortcutsWarnings() {
+ let duplicateCommands = {
+ commandOne: {
+ suggested_key: { default: "Shift+Alt+1" },
+ },
+ commandTwo: {
+ description: "Command Two!",
+ suggested_key: { default: "Shift+Alt+2" },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: duplicateCommands,
+ name: "Extension 1",
+ },
+ background() {
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ ...duplicateCommands,
+ commandThree: {
+ description: "Command Three!",
+ suggested_key: { default: "Shift+Alt+3" },
+ },
+ },
+ name: "Extension 2",
+ },
+ background() {
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension2.startup();
+ await extension2.awaitMessage("ready");
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ let warningBars = doc.querySelectorAll("moz-message-bar");
+ // Ensure warning messages are shown for each duplicate shorctut.
+ is(
+ warningBars.length,
+ Object.keys(duplicateCommands).length,
+ "There is a warning message bar for each duplicate shortcut"
+ );
+
+ // Ensure warning messages are correct with correct shortcuts.
+ let count = 1;
+ for (let warning of warningBars) {
+ let l10nAttrs = doc.l10n.getAttributes(warning);
+ await TestUtils.waitForCondition(() => warning.message !== "");
+ Assert.notStrictEqual(
+ warning.message,
+ "",
+ "Warning message attribute is set"
+ );
+ is(
+ l10nAttrs.id,
+ "shortcuts-duplicate-warning-message2",
+ "Warning message l10nId is correct"
+ );
+ Assert.deepEqual(
+ l10nAttrs.args,
+ { shortcut: `Shift+Alt+${count}` },
+ "Warning message shortcut is correct"
+ );
+ count++;
+ }
+
+ ["Shift+Alt+1", "Shift+Alt+2"].forEach((shortcut, index) => {
+ // Ensure warning messages are correct with correct shortcuts.
+ let warning = warningBars[index];
+ let l10nAttrs = doc.l10n.getAttributes(warning);
+ Assert.notStrictEqual(
+ warning.message,
+ "",
+ "Warning message attribute is set"
+ );
+ is(
+ l10nAttrs.id,
+ "shortcuts-duplicate-warning-message2",
+ "Warning message l10nId is correct"
+ );
+ Assert.deepEqual(
+ l10nAttrs.args,
+ { shortcut },
+ "Warning message shortcut is correct"
+ );
+
+ // Check if all inputs have warning style.
+ let inputs = doc.querySelectorAll(`input[shortcut="${shortcut}"]`);
+ for (let input of inputs) {
+ // Check if warning error message is shown on focus.
+ input.focus();
+ let error = doc.querySelector(".error-message");
+ let label = error.querySelector(".error-message-label");
+ is(error.style.visibility, "visible", "The error element is shown");
+ is(
+ error.getAttribute("type"),
+ "warning",
+ "Duplicate shortcut has warning class"
+ );
+ is(
+ label.dataset.l10nId,
+ "shortcuts-duplicate",
+ "Correct error message is shown"
+ );
+
+ // On keypress events wrning class should be removed.
+ EventUtils.synthesizeKey("A");
+ ok(
+ !error.classList.contains("warning"),
+ "Error element should not have warning class"
+ );
+
+ input.blur();
+ is(
+ error.style.visibility,
+ "hidden",
+ "The error element is hidden on blur"
+ );
+ }
+ });
+
+ await closeView(win);
+ await extension.unload();
+ await extension2.unload();
+});
+
+add_task(async function testDuplicateShortcutOnMacOSCtrlKey() {
+ if (AppConstants.platform !== "macosx") {
+ ok(
+ true,
+ `Skipping macos specific test on platform ${AppConstants.platform}`
+ );
+ return;
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension 1",
+ browser_specific_settings: {
+ gecko: { id: "extension1@mochi.test" },
+ },
+ commands: {
+ commandOne: {
+ // Cover expected mac normalized shortcut on default shortcut.
+ suggested_key: { default: "Ctrl+Shift+1" },
+ },
+ commandTwo: {
+ suggested_key: {
+ default: "Alt+Shift+2",
+ // Cover expected mac normalized shortcut on mac-specific shortcut.
+ mac: "Ctrl+Shift+2",
+ },
+ },
+ },
+ },
+ });
+
+ const extension2 = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension 2",
+ browser_specific_settings: {
+ gecko: { id: "extension2@mochi.test" },
+ },
+ commands: {
+ anotherCommand: {},
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension2.startup();
+
+ const win = await loadShortcutsView();
+ const doc = win.document;
+ const errorEl = doc.querySelector("addon-shortcuts .error-message");
+ const errorLabel = errorEl.querySelector(".error-message-label");
+
+ ok(
+ BrowserTestUtils.isHidden(errorEl),
+ "Expect shortcut error element to be initially hidden"
+ );
+
+ const getShortcutInput = commandName =>
+ doc.querySelector(`input.shortcut-input[name="${commandName}"]`);
+
+ const assertDuplicateShortcutWarning = async msg => {
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.isVisible(errorEl),
+ `Wait for the shortcut-duplicate error to be visible on ${msg}`
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(errorLabel),
+ {
+ id: "shortcuts-exists",
+ args: { addon: "Extension 1" },
+ },
+ `Got the expected warning message on duplicate shortcut on ${msg}`
+ );
+ };
+
+ const clearWarning = async inputEl => {
+ anotherCommandInput.blur();
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.isHidden(errorEl),
+ "Wait for the shortcut-duplicate error to be hidden"
+ );
+ };
+
+ const anotherCommandInput = getShortcutInput("anotherCommand");
+ anotherCommandInput.focus();
+ EventUtils.synthesizeKey("1", { metaKey: true, shiftKey: true });
+
+ await assertDuplicateShortcutWarning("shortcut conflict with commandOne");
+ await clearWarning(anotherCommandInput);
+
+ anotherCommandInput.focus();
+ EventUtils.synthesizeKey("2", { metaKey: true, shiftKey: true });
+
+ await assertDuplicateShortcutWarning("shortcut conflict with commandTwo");
+ await clearWarning(anotherCommandInput);
+
+ await closeView(win);
+ await extension.unload();
+ await extension2.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js
new file mode 100644
index 0000000000..f391edbf34
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const THEME_ID = "default-theme@mozilla.org";
+
+function assertViewHas(win, selector, msg) {
+ ok(win.document.querySelector(selector), msg);
+}
+function assertListView(win, type) {
+ assertViewHas(win, `addon-list[type="${type}"]`, `On ${type} list`);
+}
+
+add_task(async function testClickingSidebarEntriesChangesView() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let themeCategory = doc.querySelector("#categories > [name=theme]");
+ let extensionCategory = doc.querySelector("#categories > [name=extension]");
+
+ assertListView(win, "extension");
+
+ let loaded = waitForViewLoad(win);
+ themeCategory.click();
+ await loaded;
+
+ assertListView(win, "theme");
+
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, THEME_ID).querySelector(".addon-name-link").click();
+ await loaded;
+
+ ok(!doc.querySelector("addon-list"), "No more addon-list");
+ assertViewHas(
+ win,
+ `addon-card[addon-id="${THEME_ID}"][expanded]`,
+ "Detail view now"
+ );
+
+ loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(themeCategory, {}, win);
+ await loaded;
+
+ assertListView(win, "theme");
+
+ loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(extensionCategory, {}, win);
+ await loaded;
+
+ assertListView(win, "extension");
+
+ await closeView(win);
+});
+
+add_task(async function testClickingSidebarPaddingNoChange() {
+ let win = await loadInitialView("theme");
+ let categoryUtils = new CategoryUtilities(win);
+ let themeCategory = categoryUtils.get("theme");
+
+ let loadDetailView = async () => {
+ let loaded = waitForViewLoad(win);
+ getAddonCard(win, THEME_ID).querySelector(".addon-name-link").click();
+ await loaded;
+
+ is(
+ win.gViewController.currentViewId,
+ `addons://detail/${THEME_ID}`,
+ "The detail view loaded"
+ );
+ };
+
+ // Confirm that clicking the button directly works.
+ await loadDetailView();
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(themeCategory, {}, win);
+ await loaded;
+ is(
+ win.gViewController.currentViewId,
+ `addons://list/theme`,
+ "The detail view loaded"
+ );
+
+ // Confirm that clicking on the padding beside it does nothing.
+ await loadDetailView();
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully targeting a non-interactive padding of the container
+ // to confirm nothing happens, thus this rule check shall be ignored by
+ // a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ EventUtils.synthesizeMouse(themeCategory, -5, -5, {}, win);
+ AccessibilityUtils.resetEnv();
+ ok(!win.gViewController.isLoading, "No view is loading");
+
+ await closeView(win);
+});
+
+add_task(async function testKeyboardUsage() {
+ let win = await loadInitialView("extension");
+ let categories = win.document.getElementById("categories");
+ let extensionCategory = categories.getButtonByName("extension");
+ let themeCategory = categories.getButtonByName("theme");
+ let pluginCategory = categories.getButtonByName("plugin");
+
+ let waitForAnimationFrame = () =>
+ new Promise(resolve => win.requestAnimationFrame(resolve));
+ let sendKey = (key, e = {}) => {
+ EventUtils.synthesizeKey(key, e, win);
+ return waitForAnimationFrame();
+ };
+ let sendTabKey = e => sendKey("VK_TAB", e);
+ let isFocusInCategories = () =>
+ categories.contains(win.document.activeElement);
+
+ ok(!isFocusInCategories(), "Focus is not in the category list");
+
+ // Tab to the first focusable element.
+ await sendTabKey();
+
+ ok(isFocusInCategories(), "Focus is in the category list");
+ is(
+ win.document.activeElement,
+ extensionCategory,
+ "The extension button is focused"
+ );
+
+ // Tab out of the categories list.
+ await sendTabKey();
+ ok(!isFocusInCategories(), "Focus is out of the category list");
+
+ // Tab back into the list.
+ await sendTabKey({ shiftKey: true });
+ is(win.document.activeElement, extensionCategory, "Back on Extensions");
+
+ // We're on the extension list.
+ assertListView(win, "extension");
+
+ // Switch to theme list.
+ let loaded = waitForViewLoad(win);
+ await sendKey("VK_DOWN");
+ is(win.document.activeElement, themeCategory, "Themes is focused");
+ await loaded;
+
+ assertListView(win, "theme");
+
+ loaded = waitForViewLoad(win);
+ await sendKey("VK_DOWN");
+ is(win.document.activeElement, pluginCategory, "Plugins is focused");
+ await loaded;
+
+ assertListView(win, "plugin");
+
+ await sendKey("VK_DOWN");
+ is(win.document.activeElement, pluginCategory, "Plugins is still focused");
+ ok(!win.gViewController.isLoading, "No view is loading");
+
+ loaded = waitForViewLoad(win);
+ await sendKey("VK_UP");
+ await loaded;
+ loaded = waitForViewLoad(win);
+ await sendKey("VK_UP");
+ await loaded;
+ is(win.document.activeElement, extensionCategory, "Extensions is focused");
+ assertListView(win, "extension");
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js
new file mode 100644
index 0000000000..4cb641c2a0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the visible delay in showing the "Language" category occurs
+// very minimally
+
+let gProvider;
+let gInstall;
+let gInstallProperties = [
+ {
+ name: "Locale Category Test",
+ type: "locale",
+ },
+];
+
+function installLocale() {
+ return new Promise(resolve => {
+ gInstall = gProvider.createInstalls(gInstallProperties)[0];
+ gInstall.addTestListener({
+ onInstallEnded(aInstall) {
+ gInstall.removeTestListener(this);
+ resolve();
+ },
+ });
+ gInstall.install();
+ });
+}
+
+async function checkCategory(win, category, { expectHidden, expectSelected }) {
+ await win.customElements.whenDefined("categories-box");
+
+ let categoriesBox = win.document.getElementById("categories");
+ await categoriesBox.promiseRendered;
+
+ let button = categoriesBox.getButtonByName(category);
+ is(
+ button.hidden,
+ expectHidden,
+ `${category} button is ${expectHidden ? "" : "not "}hidden`
+ );
+ if (expectSelected !== undefined) {
+ is(
+ button.selected,
+ expectSelected,
+ `${category} button is ${expectSelected ? "" : "not "}selected`
+ );
+ }
+}
+
+add_setup(async function () {
+ gProvider = new MockProvider();
+});
+
+add_task(async function testLocalesHiddenByDefault() {
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", { expectHidden: true });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", { expectHidden: true });
+
+ await installLocale();
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesShownWhenInstalled() {
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesHiddenWhenUninstalled() {
+ gInstall.cancel();
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", { expectHidden: true });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesHiddenWithoutDelay() {
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", { expectHidden: true });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", { expectHidden: true });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesShownAfterDelay() {
+ await installLocale();
+
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", { expectHidden: true });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesShownIfPreviousView() {
+ gProvider.blockQueryResponses();
+
+ // Passing "locale" will set the last view to locales and open the view.
+ let viewLoaded = loadInitialView("locale", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: true,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: true,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesHiddenIfPreviousViewAndNoLocales() {
+ gInstall.cancel();
+ gProvider.blockQueryResponses();
+
+ // Passing "locale" will set the last view to locales and open the view.
+ let viewLoaded = loadInitialView("locale", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: true,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ let categoryUtils = new CategoryUtilities(win);
+
+ await TestUtils.waitForCondition(
+ () => categoryUtils.selectedCategory != "locale"
+ );
+
+ await checkCategory(win, "locale", {
+ expectHidden: true,
+ expectSelected: false,
+ });
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ win.gViewController.defaultViewId,
+ "default view is selected"
+ );
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js
new file mode 100644
index 0000000000..4c5b1e25f0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that the selected category is persisted across loads of the manager
+
+add_task(async function testCategoryRestore() {
+ let win = await loadInitialView("extension");
+ let utils = new CategoryUtilities(win);
+
+ // Open the plugins category
+ await utils.openType("plugin");
+
+ // Re-open the manager
+ await closeView(win);
+ win = await loadInitialView();
+ utils = new CategoryUtilities(win);
+
+ is(
+ utils.selectedCategory,
+ "plugin",
+ "Should have shown the plugins category"
+ );
+
+ // Open the extensions category
+ await utils.openType("extension");
+
+ // Re-open the manager
+ await closeView(win);
+ win = await loadInitialView();
+ utils = new CategoryUtilities(win);
+
+ is(
+ utils.selectedCategory,
+ "extension",
+ "Should have shown the extensions category"
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testInvalidAddonType() {
+ let win = await loadInitialView("invalid");
+
+ let categoryUtils = new CategoryUtilities(win);
+ is(
+ categoryUtils.getSelectedViewId(),
+ win.gViewController.defaultViewId,
+ "default view is selected"
+ );
+ is(
+ win.gViewController.currentViewId,
+ win.gViewController.defaultViewId,
+ "default view is shown"
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testInvalidViewId() {
+ let win = await loadInitialView("addons://invalid/view");
+
+ let categoryUtils = new CategoryUtilities(win);
+ is(
+ categoryUtils.getSelectedViewId(),
+ win.gViewController.defaultViewId,
+ "default view is selected"
+ );
+ is(
+ win.gViewController.currentViewId,
+ win.gViewController.defaultViewId,
+ "default view is shown"
+ );
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_subframe_install.js b/toolkit/mozapps/extensions/test/browser/browser_subframe_install.js
new file mode 100644
index 0000000000..e9e8c73728
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_subframe_install.js
@@ -0,0 +1,234 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+
+AddonTestUtils.initMochitest(this);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.install.requireBuiltInCerts", false]],
+ });
+
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+function testSubframeInstallOnNavigation({
+ topFrameURL,
+ midFrameURL,
+ bottomFrameURL,
+ xpiURL,
+ assertFn,
+}) {
+ return BrowserTestUtils.withNewTab(topFrameURL, async browser => {
+ await SpecialPowers.pushPrefEnv({
+ // Relax the user input requirements while running this test.
+ set: [["xpinstall.userActivation.required", false]],
+ });
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: [`${midFrameURL}*`],
+ js: ["createFrame.js"],
+ all_frames: true,
+ },
+ {
+ matches: [`${bottomFrameURL}*`],
+ js: ["installByNavigatingToXPIURL.js"],
+ all_frames: true,
+ },
+ ],
+ },
+ files: {
+ "createFrame.js": `(function(frameURL) {
+ browser.test.log("Executing createFrame.js on " + window.location.href);
+ const frame = document.createElement("iframe");
+ frame.src = frameURL;
+ document.body.appendChild(frame);
+ })("${bottomFrameURL}")`,
+
+ "installByNavigatingToXPIURL.js": `
+ browser.test.log("Navigating to XPI url from " + window.location.href);
+ const link = document.createElement("a");
+ link.id = "xpi-link";
+ link.href = "${xpiURL}";
+ link.textContent = "Link to XPI file";
+ document.body.appendChild(link);
+ link.click();
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.spawn(browser, [midFrameURL], async frameURL => {
+ const frame = content.document.createElement("iframe");
+ frame.src = frameURL;
+ content.document.body.appendChild(frame);
+ });
+
+ await assertFn({ browser });
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+ });
+}
+
+add_task(async function testInstallBlockedOnNavigationFromCrossOriginFrame() {
+ const promiseOriginBlocked = TestUtils.topicObserved(
+ "addon-install-origin-blocked"
+ );
+
+ await testSubframeInstallOnNavigation({
+ topFrameURL: "https://test1.example.com/",
+ midFrameURL: "https://example.org/",
+ bottomFrameURL: "https://test1.example.com/installTrigger",
+ xpiURL: XPI_URL,
+ assertFn: async () => {
+ await promiseOriginBlocked;
+ Assert.deepEqual(
+ await AddonManager.getAllInstalls(),
+ [],
+ "Expects no pending addon install"
+ );
+ },
+ });
+});
+
+add_task(async function testInstallPromptedOnNavigationFromSameOriginFrame() {
+ const promisePromptedInstallFromThirdParty = TestUtils.topicObserved(
+ "addon-install-blocked"
+ );
+
+ await testSubframeInstallOnNavigation({
+ topFrameURL: "https://test2.example.com/",
+ midFrameURL: "https://test1.example.com/",
+ bottomFrameURL: "https://test2.example.com/installTrigger",
+ xpiURL: XPI_URL,
+ assertFn: async () => {
+ const [subject] = await promisePromptedInstallFromThirdParty;
+ let installInfo = subject.wrappedJSObject;
+ ok(installInfo, "Got a blocked addon install pending");
+ installInfo.cancel();
+ },
+ });
+});
+
+add_task(async function testInstallTriggerBlockedFromCrossOriginFrame() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+
+ const promiseOriginBlocked = TestUtils.topicObserved(
+ "addon-install-origin-blocked"
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["https://example.org/*"],
+ js: ["createFrame.js"],
+ all_frames: true,
+ },
+ {
+ matches: ["https://test1.example.com/installTrigger*"],
+ js: ["installTrigger.js"],
+ all_frames: true,
+ },
+ ],
+ },
+ files: {
+ "createFrame.js": function () {
+ const frame = document.createElement("iframe");
+ frame.src = "https://test1.example.com/installTrigger/";
+ document.body.appendChild(frame);
+ },
+ "installTrigger.js": `
+ window.InstallTrigger.install({extension: "${XPI_URL}"});
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com",
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ const frame = content.document.createElement("iframe");
+ frame.src = "https://example.org";
+ content.document.body.appendChild(frame);
+ });
+
+ await promiseOriginBlocked;
+ Assert.deepEqual(
+ await AddonManager.getAllInstalls(),
+ [],
+ "Expects no pending addon install"
+ );
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testInstallTriggerPromptedFromSameOriginFrame() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+
+ const promisePromptedInstallFromThirdParty = TestUtils.topicObserved(
+ "addon-install-blocked"
+ );
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ await SpecialPowers.spawn(browser, [XPI_URL], async xpiURL => {
+ const frame = content.document.createElement("iframe");
+ frame.src = "https://example.com";
+ const frameLoaded = new Promise(resolve => {
+ frame.addEventListener("load", resolve, { once: true });
+ });
+ content.document.body.appendChild(frame);
+ await frameLoaded;
+ frame.contentWindow.InstallTrigger.install({ URL: xpiURL });
+ });
+
+ const [subject] = await promisePromptedInstallFromThirdParty;
+ let installInfo = subject.wrappedJSObject;
+ ok(installInfo, "Got a blocked addon install pending");
+ is(
+ installInfo?.installs?.[0]?.state,
+ Services.prefs.getBoolPref(
+ "extensions.postDownloadThirdPartyPrompt",
+ false
+ )
+ ? AddonManager.STATE_DOWNLOADED
+ : AddonManager.STATE_AVAILABLE,
+ "Got a pending addon install"
+ );
+ await installInfo.cancel();
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
new file mode 100644
index 0000000000..f8e3293b82
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that we throw if a test created with add_task()
+// calls run_next_test
+
+add_task(async function run_next_throws() {
+ let err = null;
+ try {
+ run_next_test();
+ } catch (e) {
+ err = e;
+ info("run_next_test threw " + err);
+ }
+ ok(err, "run_next_test() should throw an error inside an add_task test");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updateid.js b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
new file mode 100644
index 0000000000..c6e6d3030f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that updates that change an add-on's ID show up correctly in the UI
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+
+function getName(item) {
+ return item.addonNameEl.textContent;
+}
+
+async function getUpdateButton(item) {
+ let button = item.querySelector('[action="install-update"]');
+ let panel = button.closest("panel-list");
+ let shown = BrowserTestUtils.waitForEvent(panel, "shown");
+ let moreOptionsButton = item.querySelector('[action="more-options"]');
+ EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, item.ownerGlobal);
+ await shown;
+ return button;
+}
+
+add_task(async function test_updateid() {
+ // Close the existing about:addons tab and unrestier the existing MockProvider
+ // instance if a previous failed test has not been able to clear them.
+ if (gManagerWindow) {
+ await close_manager(gManagerWindow);
+ }
+ if (gProvider) {
+ gProvider.unregister();
+ }
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([
+ {
+ id: "addon1@tests.mozilla.org",
+ name: "manually updating addon",
+ version: "1.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE,
+ },
+ ]);
+
+ gManagerWindow = await open_manager("addons://list/extension");
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ await gCategoryUtilities.openType("extension");
+
+ gProvider.createInstalls([
+ {
+ name: "updated add-on",
+ existingAddon: gProvider.addons[0],
+ version: "2.0",
+ },
+ ]);
+ var newAddon = new MockAddon("addon2@tests.mozilla.org");
+ newAddon.name = "updated add-on";
+ newAddon.version = "2.0";
+ newAddon.pendingOperations = AddonManager.PENDING_INSTALL;
+ gProvider.installs[0]._addonToInstall = newAddon;
+
+ var item = getAddonCard(gManagerWindow, "addon1@tests.mozilla.org");
+ is(
+ getName(item),
+ "manually updating addon",
+ "Should show the old name in the list"
+ );
+ const { name, version } = await get_tooltip_info(item, gManagerWindow);
+ is(
+ name,
+ "manually updating addon",
+ "Should show the old name in the tooltip"
+ );
+ is(version, "1.0", "Should still show the old version in the tooltip");
+
+ var update = await getUpdateButton(item);
+ is_element_visible(update, "Update button should be visible");
+
+ item = getAddonCard(gManagerWindow, "addon2@tests.mozilla.org");
+ is(item, null, "Should not show the new version in the list");
+
+ await close_manager(gManagerWindow);
+ gManagerWindow = null;
+ gProvider.unregister();
+ gProvider = null;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.js b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js
new file mode 100644
index 0000000000..9dbeec4a84
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js
@@ -0,0 +1,389 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+let { AddonUpdateChecker } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/AddonUpdateChecker.sys.mjs"
+);
+
+const updatejson = RELATIVE_DIR + "browser_updatessl.json";
+const redirect = RELATIVE_DIR + "redirect.sjs?";
+const SUCCESS = 0;
+const DOWNLOAD_ERROR = AddonManager.ERROR_DOWNLOAD_ERROR;
+
+const HTTP = "http://example.com/";
+const HTTPS = "https://example.com/";
+const NOCERT = "https://nocert.example.com/";
+const SELFSIGNED = "https://self-signed.example.com/";
+const UNTRUSTED = "https://untrusted.example.com/";
+const EXPIRED = "https://expired.example.com/";
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+var gTests = [];
+var gStart = 0;
+var gLast = 0;
+
+var HTTPObserver = {
+ observeActivity(
+ aChannel,
+ aType,
+ aSubtype,
+ aTimestamp,
+ aSizeData,
+ aStringData
+ ) {
+ aChannel.QueryInterface(Ci.nsIChannel);
+
+ dump(
+ "*** HTTP Activity 0x" +
+ aType.toString(16) +
+ " 0x" +
+ aSubtype.toString(16) +
+ " " +
+ aChannel.URI.spec +
+ "\n"
+ );
+ },
+};
+
+function test() {
+ gStart = Date.now();
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ observerService.addObserver(HTTPObserver);
+
+ registerCleanupFunction(function () {
+ observerService.removeObserver(HTTPObserver);
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ var cos = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+ cos.clearValidityOverride("nocert.example.com", -1, {});
+ cos.clearValidityOverride("self-signed.example.com", -1, {});
+ cos.clearValidityOverride("untrusted.example.com", -1, {});
+ cos.clearValidityOverride("expired.example.com", -1, {});
+
+ info("All tests completed in " + (Date.now() - gStart) + "ms");
+ finish();
+}
+
+function add_update_test(mainURL, redirectURL, expectedStatus) {
+ gTests.push([mainURL, redirectURL, expectedStatus]);
+}
+
+function run_update_tests(callback) {
+ function run_next_update_test() {
+ if (!gTests.length) {
+ callback();
+ return;
+ }
+ gLast = Date.now();
+
+ let [mainURL, redirectURL, expectedStatus] = gTests.shift();
+ if (redirectURL) {
+ var url = mainURL + redirect + redirectURL + updatejson;
+ var message =
+ "Should have seen the right result for an update check redirected from " +
+ mainURL +
+ " to " +
+ redirectURL;
+ } else {
+ url = mainURL + updatejson;
+ message =
+ "Should have seen the right result for an update check from " + mainURL;
+ }
+
+ AddonUpdateChecker.checkForUpdates("addon1@tests.mozilla.org", url, {
+ onUpdateCheckComplete(updates) {
+ is(updates.length, 1, "Should be the right number of results");
+ is(SUCCESS, expectedStatus, message);
+ info("Update test ran in " + (Date.now() - gLast) + "ms");
+ run_next_update_test();
+ },
+
+ onUpdateCheckError(status) {
+ is(status, expectedStatus, message);
+ info("Update test ran in " + (Date.now() - gLast) + "ms");
+ run_next_update_test();
+ },
+ });
+ }
+
+ run_next_update_test();
+}
+
+// Runs tests with built-in certificates required and no certificate exceptions.
+add_test(async function test_builtin_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, true]],
+ });
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates and no certificate
+// exceptions.
+add_test(async function test_builtin_not_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, SUCCESS);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, SUCCESS);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Set up overrides for the next test.
+add_test(() => {
+ addCertOverrides().then(run_next_test);
+});
+
+// Runs tests with built-in certificates required and all certificate exceptions.
+add_test(async function test_builtin_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, true]],
+ });
+
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, SUCCESS);
+ add_update_test(HTTP, SELFSIGNED, SUCCESS);
+ add_update_test(HTTP, UNTRUSTED, SUCCESS);
+ add_update_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates and all certificate
+// exceptions.
+add_test(async function test_builtin_not_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, SUCCESS);
+ add_update_test(NOCERT, null, SUCCESS);
+ add_update_test(SELFSIGNED, null, SUCCESS);
+ add_update_test(UNTRUSTED, null, SUCCESS);
+ add_update_test(EXPIRED, null, SUCCESS);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, SUCCESS);
+ add_update_test(HTTP, SELFSIGNED, SUCCESS);
+ add_update_test(HTTP, UNTRUSTED, SUCCESS);
+ add_update_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, SUCCESS);
+ add_update_test(HTTPS, NOCERT, SUCCESS);
+ add_update_test(HTTPS, SELFSIGNED, SUCCESS);
+ add_update_test(HTTPS, UNTRUSTED, SUCCESS);
+ add_update_test(HTTPS, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, SUCCESS);
+ add_update_test(NOCERT, NOCERT, SUCCESS);
+ add_update_test(NOCERT, SELFSIGNED, SUCCESS);
+ add_update_test(NOCERT, UNTRUSTED, SUCCESS);
+ add_update_test(NOCERT, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, SUCCESS);
+ add_update_test(SELFSIGNED, NOCERT, SUCCESS);
+ add_update_test(SELFSIGNED, SELFSIGNED, SUCCESS);
+ add_update_test(SELFSIGNED, UNTRUSTED, SUCCESS);
+ add_update_test(SELFSIGNED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, SUCCESS);
+ add_update_test(UNTRUSTED, NOCERT, SUCCESS);
+ add_update_test(UNTRUSTED, SELFSIGNED, SUCCESS);
+ add_update_test(UNTRUSTED, UNTRUSTED, SUCCESS);
+ add_update_test(UNTRUSTED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, SUCCESS);
+ add_update_test(EXPIRED, NOCERT, SUCCESS);
+ add_update_test(EXPIRED, SELFSIGNED, SUCCESS);
+ add_update_test(EXPIRED, UNTRUSTED, SUCCESS);
+ add_update_test(EXPIRED, EXPIRED, SUCCESS);
+
+ run_update_tests(run_next_test);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.json b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json
new file mode 100644
index 0000000000..223d1ef2d3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json
@@ -0,0 +1,17 @@
+{
+ "addons": {
+ "addon1@tests.mozilla.org": {
+ "updates": [
+ {
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0",
+ "advisory_max_version": "20"
+ }
+ },
+ "version": "2.0"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^
new file mode 100644
index 0000000000..2e4f8163bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^
@@ -0,0 +1 @@
+Connection: close
diff --git a/toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js b/toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js
new file mode 100644
index 0000000000..e245e3a6e4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineESModuleGetters(this, {
+ BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs",
+});
+
+// Maps add-on descriptors to updated Fluent IDs. Keep it in sync
+// with the list in XPIDatabase.sys.mjs.
+const updatedAddonFluentIds = new Map([
+ ["extension-default-theme-name", "extension-default-theme-name-auto"],
+]);
+
+add_task(async function test_ensure_bundled_addons_are_localized() {
+ let l10nReg = L10nRegistry.getInstance();
+ let bundles = l10nReg.generateBundlesSync(
+ ["en-US"],
+ ["browser/appExtensionFields.ftl"]
+ );
+ let addons = await AddonManager.getAllAddons();
+ let standardBuiltInThemes = addons.filter(
+ addon =>
+ addon.isBuiltin &&
+ addon.type === "theme" &&
+ !addon.id.endsWith("colorway@mozilla.org")
+ );
+ let bundle = bundles.next().value;
+
+ ok(!!standardBuiltInThemes.length, "Standard built-in themes should exist");
+
+ for (let standardTheme of standardBuiltInThemes) {
+ let l10nId = standardTheme.id.replace("@mozilla.org", "");
+ for (let prop of ["name", "description"]) {
+ let defaultFluentId = `extension-${l10nId}-${prop}`;
+ let fluentId =
+ updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId;
+ ok(
+ bundle.hasMessage(fluentId),
+ `l10n id for ${standardTheme.id} \"${prop}\" attribute should exist`
+ );
+ }
+ }
+
+ let colorwayThemes = Array.from(BuiltInThemes.builtInThemeMap.keys()).filter(
+ id => id.endsWith("colorway@mozilla.org")
+ );
+ ok(!!colorwayThemes.length, "Colorway themes should exist");
+ for (let id of colorwayThemes) {
+ let l10nId = id.replace("@mozilla.org", "");
+ let [, variantName] = l10nId.split("-", 2);
+ if (variantName != "colorway") {
+ let defaultFluentId = `extension-colorways-${variantName}-name`;
+ let fluentId =
+ updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId;
+ ok(
+ bundle.hasMessage(fluentId),
+ `l10n id for ${id} \"name\" attribute should exist`
+ );
+ }
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi.js b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
new file mode 100644
index 0000000000..853cd3902a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+function testWithAPI(task) {
+ return async function () {
+ await BrowserTestUtils.withNewTab(TESTPAGE, task);
+ };
+}
+
+let gProvider = new MockProvider();
+
+let addons = gProvider.createAddons([
+ {
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on 1",
+ version: "2.1",
+ description: "Short description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+ },
+ {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ version: "5.3.7ab",
+ description: null,
+ type: "theme",
+ userDisabled: false,
+ isActive: false,
+ },
+ {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: true,
+ isActive: false,
+ },
+ {
+ id: "addon4@tests.mozilla.org",
+ name: "Test add-on 4",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+ },
+]);
+
+addons[3].permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+
+function API_getAddonByID(browser, id) {
+ return SpecialPowers.spawn(browser, [id], async function (id) {
+ let addon = await content.navigator.mozAddonManager.getAddonByID(id);
+ let addonDetails = {};
+ for (let prop in addon) {
+ addonDetails[prop] = addon[prop];
+ }
+ // We can't send native objects back so clone its properties.
+ return JSON.parse(JSON.stringify(addonDetails));
+ });
+}
+
+add_task(
+ testWithAPI(async function (browser) {
+ function compareObjects(web, real) {
+ ok(
+ !!Object.keys(web).length,
+ "Got a valid mozAddonManager addon object dump"
+ );
+
+ for (let prop of Object.keys(web)) {
+ let webVal = web[prop];
+ let realVal = real[prop];
+
+ switch (prop) {
+ case "isEnabled":
+ realVal = !real.userDisabled;
+ break;
+
+ case "canUninstall":
+ realVal = Boolean(
+ real.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+ break;
+ }
+
+ // null and undefined don't compare well so stringify them first
+ if (realVal === null || realVal === undefined) {
+ realVal = `${realVal}`;
+ webVal = `${webVal}`;
+ }
+
+ is(
+ webVal,
+ realVal,
+ `Property ${prop} should have the right value in add-on ${real.id}`
+ );
+ }
+ }
+
+ let [a1, a2, a3] = await promiseAddonsByIDs([
+ "addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ ]);
+ let w1 = await API_getAddonByID(browser, "addon1@tests.mozilla.org");
+ let w2 = await API_getAddonByID(browser, "addon2@tests.mozilla.org");
+ let w3 = await API_getAddonByID(browser, "addon3@tests.mozilla.org");
+
+ compareObjects(w1, a1);
+ compareObjects(w2, a2);
+ compareObjects(w3, a3);
+ })
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js
new file mode 100644
index 0000000000..b9ea0f6a93
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js
@@ -0,0 +1,375 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+loadTestSubscript("head_abuse_report.js");
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const TELEMETRY_EVENTS_FILTERS = {
+ category: "addonsManager",
+ method: "report",
+};
+const REPORT_PROP_NAMES = [
+ "addon",
+ "addon_signature",
+ "reason",
+ "message",
+ "report_entry_point",
+];
+
+function getObjectProps(obj, propNames) {
+ const res = {};
+ for (const k of propNames) {
+ res[k] = obj[k];
+ }
+ return res;
+}
+
+async function assertSubmittedReport(expectedReportProps) {
+ let reportSubmitted;
+ const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled(
+ ({ data, request, response }) => {
+ reportSubmitted = JSON.parse(data);
+ handleSubmitRequest({ request, response });
+ }
+ );
+
+ let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ let promiseWinClosed = waitClosedWindow();
+ let promisePanelUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ panelEl,
+ "submit"
+ );
+ panelEl._form.elements.reason.value = expectedReportProps.reason;
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnNext);
+ await promisePanelUpdated;
+
+ panelEl._form.elements.message.value = expectedReportProps.message;
+ // Reset the timestamp of the last report between tests.
+ AbuseReporter._lastReportTimestamp = null;
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnSubmit);
+ await Promise.all([onReportSubmitted, promiseWinClosed]);
+
+ ok(!panelEl.ownerGlobal, "Report dialog window is closed");
+ Assert.deepEqual(
+ getObjectProps(reportSubmitted, REPORT_PROP_NAMES),
+ expectedReportProps,
+ "Got the expected report data submitted"
+ );
+}
+
+add_setup(async function () {
+ await AbuseReportTestUtils.setup();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.abuseReport.amWebAPI.enabled", true],
+ // Make sure the integrated abuse report panel is the one enabled
+ // while this test file runs (instead of the AMO hosted form).
+ // NOTE: behaviors expected when amoFormEnabled is true are tested
+ // in the separate browser_amo_abuse_report.js test file.
+ ["extensions.abuseReport.amoFormEnabled", false],
+ ],
+ });
+});
+
+add_task(async function test_report_installed_addon_cancelled() {
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const extension = await installTestExtension(ADDON_ID);
+
+ let reportEnabled = await SpecialPowers.spawn(browser, [], () => {
+ return content.navigator.mozAddonManager.abuseReportPanelEnabled;
+ });
+
+ is(reportEnabled, true, "Expect abuseReportPanelEnabled to be true");
+
+ info("Test reportAbuse result on user cancelled report");
+
+ let promiseNewWindow = waitForNewWindow();
+ let promiseWebAPIResult = SpecialPowers.spawn(
+ browser,
+ [ADDON_ID],
+ addonId => content.navigator.mozAddonManager.reportAbuse(addonId)
+ );
+
+ let win = await promiseNewWindow;
+ is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
+
+ let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ let promiseWinClosed = waitClosedWindow();
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel);
+ let reportResult = await promiseWebAPIResult;
+ is(
+ reportResult,
+ false,
+ "Expect reportAbuse to resolve to false on user cancelled report"
+ );
+ await promiseWinClosed;
+ ok(!panelEl.ownerGlobal, "Report dialog window is closed");
+
+ await extension.unload();
+ });
+
+ // Expect no telemetry events collected for user cancelled reports.
+ TelemetryTestUtils.assertEvents([], TELEMETRY_EVENTS_FILTERS);
+});
+
+add_task(async function test_report_installed_addon_submitted() {
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const extension = await installTestExtension(ADDON_ID);
+
+ let promiseNewWindow = waitForNewWindow();
+ let promiseWebAPIResult = SpecialPowers.spawn(browser, [ADDON_ID], id =>
+ content.navigator.mozAddonManager.reportAbuse(id)
+ );
+ let win = await promiseNewWindow;
+ is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
+
+ await assertSubmittedReport({
+ addon: ADDON_ID,
+ addon_signature: "missing",
+ message: "fake report message",
+ reason: "unwanted",
+ report_entry_point: "amo",
+ });
+
+ let reportResult = await promiseWebAPIResult;
+ is(
+ reportResult,
+ true,
+ "Expect reportAbuse to resolve to false on user cancelled report"
+ );
+
+ await extension.unload();
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "amo",
+ value: ADDON_ID,
+ extra: { addon_type: "extension" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+});
+
+add_task(async function test_report_unknown_not_installed_addon() {
+ const addonId = "unknown-addon@mochi.test";
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id =>
+ content.navigator.mozAddonManager.reportAbuse(id).catch(err => {
+ return { name: err.name, message: err.message };
+ })
+ );
+
+ await Assert.deepEqual(
+ await promiseWebAPIResult,
+ { name: "Error", message: "Error creating abuse report" },
+ "Got the expected rejected error on reporting unknown addon"
+ );
+
+ ok(!AbuseReportTestUtils.getReportDialog(), "No report dialog is open");
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "amo",
+ value: addonId,
+ extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" },
+ },
+ {
+ object: "amo",
+ value: addonId,
+ extra: { error_type: "ERROR_ADDON_NOTFOUND" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+});
+
+add_task(async function test_report_not_installed_addon() {
+ const addonId = "not-installed-addon@mochi.test";
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const fakeAMODetails = {
+ name: "fake name",
+ current_version: { version: "1.0" },
+ type: "extension",
+ icon_url: "http://test.addons.org/asserts/fake-icon-url.png",
+ homepage: "http://fake.url/homepage",
+ authors: [{ name: "author1", url: "http://fake.url/author1" }],
+ is_recommended: false,
+ };
+
+ AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
+ registerCleanupFunction(() =>
+ AbuseReportTestUtils.amoAddonDetailsMap.clear()
+ );
+
+ let promiseNewWindow = waitForNewWindow();
+
+ let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id =>
+ content.navigator.mozAddonManager.reportAbuse(id)
+ );
+ let win = await promiseNewWindow;
+ is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
+
+ await assertSubmittedReport({
+ addon: addonId,
+ addon_signature: "unknown",
+ message: "fake report message",
+ reason: "other",
+ report_entry_point: "amo",
+ });
+
+ let reportResult = await promiseWebAPIResult;
+ is(
+ reportResult,
+ true,
+ "Expect reportAbuse to resolve to true on submitted report"
+ );
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "amo",
+ value: addonId,
+ extra: { addon_type: "extension" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+});
+
+add_task(async function test_amo_report_on_report_already_inprogress() {
+ const extension = await installTestExtension(ADDON_ID);
+ const reportDialog = await AbuseReporter.openDialog(
+ ADDON_ID,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+ await AbuseReportTestUtils.promiseReportDialogRendered();
+ ok(reportDialog.window, "Got an open report dialog");
+
+ let promiseWinClosed = waitClosedWindow();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const promiseAMOResult = SpecialPowers.spawn(browser, [ADDON_ID], id =>
+ content.navigator.mozAddonManager.reportAbuse(id)
+ );
+
+ await promiseWinClosed;
+ ok(reportDialog.window.closed, "previous report dialog should be closed");
+
+ is(
+ await reportDialog.promiseAMOResult,
+ undefined,
+ "old report cancelled after AMO called mozAddonManager.reportAbuse"
+ );
+
+ const panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ const { report } = AbuseReportTestUtils.getReportDialogParams();
+ Assert.deepEqual(
+ {
+ reportEntryPoint: report.reportEntryPoint,
+ addonId: report.addon.id,
+ },
+ {
+ reportEntryPoint: "amo",
+ addonId: ADDON_ID,
+ },
+ "Got the expected report from the opened report dialog"
+ );
+
+ promiseWinClosed = waitClosedWindow();
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel);
+ await promiseWinClosed;
+
+ is(
+ await promiseAMOResult,
+ false,
+ "AMO report request resolved to false on cancel button clicked"
+ );
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_reject_on_unsupported_addon_types() {
+ const addonId = "not-supported-addon-type@mochi.test";
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const fakeAMODetails = {
+ name: "fake name",
+ current_version: { version: "1.0" },
+ type: "fake-unsupported-addon-type",
+ };
+
+ AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
+ registerCleanupFunction(() =>
+ AbuseReportTestUtils.amoAddonDetailsMap.clear()
+ );
+
+ let webAPIResult = await SpecialPowers.spawn(browser, [addonId], id =>
+ content.navigator.mozAddonManager.reportAbuse(id).then(
+ res => ({ gotRejection: false, result: res }),
+ err => ({ gotRejection: true, message: err.message })
+ )
+ );
+
+ Assert.deepEqual(
+ webAPIResult,
+ { gotRejection: true, message: "Error creating abuse report" },
+ "Got the expected rejection from mozAddonManager.reportAbuse"
+ );
+ });
+});
+
+add_task(async function test_report_on_disabled_webapi() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amWebAPI.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ let reportEnabled = await SpecialPowers.spawn(browser, [], () => {
+ return content.navigator.mozAddonManager.abuseReportPanelEnabled;
+ });
+
+ is(reportEnabled, false, "Expect abuseReportPanelEnabled to be false");
+
+ info("Test reportAbuse result on report webAPI disabled");
+
+ let promiseWebAPIResult = SpecialPowers.spawn(
+ browser,
+ ["an-addon@mochi.test"],
+ addonId =>
+ content.navigator.mozAddonManager.reportAbuse(addonId).catch(err => {
+ return { name: err.name, message: err.message };
+ })
+ );
+
+ Assert.deepEqual(
+ await promiseWebAPIResult,
+ { name: "Error", message: "amWebAPI reportAbuse not supported" },
+ "Got the expected rejected error"
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
new file mode 100644
index 0000000000..aec6ddedca
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function check_frame_availability(browser) {
+ return check_availability(browser.browsingContext.children[0]);
+}
+
+function check_availability(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ return content.document.getElementById("result").textContent == "true";
+ });
+}
+
+// Test that initially the API isn't available in the test domain
+add_task(async function test_not_available() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that with testing on the API is available in the test domain
+add_task(async function test_available() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(available, "API should be available.");
+ }
+ );
+});
+
+// Test that the API is not available in a bad domain
+add_task(async function test_bad_domain() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT2}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that the API is only available in https sites
+add_task(async function test_not_available_http() {
+ await BrowserTestUtils.withNewTab(
+ `${TESTROOT}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that the API is available when in a frame of the test domain
+add_task(async function test_available_framed() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT}webapi_checkframed.html`,
+ async function test_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(available, "API should be available.");
+ }
+ );
+});
+
+// Test that if the external frame is http then the inner frame doesn't have
+// the API
+add_task(async function test_not_available_http_framed() {
+ await BrowserTestUtils.withNewTab(
+ `${TESTROOT}webapi_checkframed.html`,
+ async function test_not_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that if the external frame is a bad domain then the inner frame doesn't
+// have the API
+add_task(async function test_not_available_framed() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT2}webapi_checkframed.html`,
+ async function test_not_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that a window navigated to a bad domain doesn't allow access to the API
+add_task(async function test_navigated_window() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT2}webapi_checknavigatedwindow.html`,
+ async function test_available(browser) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ await content.wrappedJSObject.openWindow();
+ });
+
+ // Should be a new tab open
+ let tab = await tabPromise;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.getBrowserForTab(tab)
+ );
+
+ SpecialPowers.spawn(browser, [], async function () {
+ content.wrappedJSObject.navigate();
+ });
+
+ await loadPromise;
+
+ let available = await SpecialPowers.spawn(browser, [], async function () {
+ return content.wrappedJSObject.check();
+ });
+
+ ok(!available, "API should not be available.");
+
+ gBrowser.removeTab(tab);
+ }
+ );
+});
+
+// Check that if a page is embedded in a chrome content UI that it can still
+// access the API.
+add_task(async function test_chrome_frame() {
+ SpecialPowers.pushPrefEnv({
+ set: [["security.allow_unsafe_parent_loads", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ `${CHROMEROOT}webapi_checkchromeframe.xhtml`,
+ async function test_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(available, "API should be available.");
+ }
+ );
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
new file mode 100644
index 0000000000..3692644714
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
@@ -0,0 +1,124 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+async function getListenerEvents(browser) {
+ let result = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split("\n").map(JSON.parse);
+}
+
+const RESTARTLESS_ID = "restartless@tests.mozilla.org";
+const INSTALL_ID = "install@tests.mozilla.org";
+const CANCEL_ID = "cancel@tests.mozilla.org";
+
+let provider = new MockProvider();
+provider.createAddons([
+ {
+ id: RESTARTLESS_ID,
+ name: "Restartless add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+ {
+ id: CANCEL_ID,
+ name: "Add-on for uninstall cancel",
+ },
+]);
+
+// Test enable/disable events for restartless
+add_task(async function test_restartless() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) {
+ let addon = await promiseAddonByID(RESTARTLESS_ID);
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // disable it
+ await addon.disable();
+ is(addon.userDisabled, true, "addon was disabled successfully");
+
+ // re-enable it
+ await addon.enable();
+ is(addon.userDisabled, false, "addon was re-enabled successfuly");
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: RESTARTLESS_ID, event: "onDisabling" },
+ { id: RESTARTLESS_ID, event: "onDisabled" },
+ { id: RESTARTLESS_ID, event: "onEnabling" },
+ { id: RESTARTLESS_ID, event: "onEnabled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable/enable events");
+ });
+});
+
+// Test install events
+add_task(async function test_restartless() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) {
+ let addon = new MockAddon(
+ INSTALL_ID,
+ "installme",
+ null,
+ AddonManager.OP_NEED_RESTART_NONE
+ );
+ let install = new MockInstall(null, null, addon);
+
+ let installPromise = new Promise(resolve => {
+ install.addTestListener({
+ onInstallEnded: resolve,
+ });
+ });
+
+ provider.addInstall(install);
+ install.install();
+
+ await installPromise;
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: INSTALL_ID, event: "onInstalling" },
+ { id: INSTALL_ID, event: "onInstalled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected install events");
+ });
+});
+
+// Test uninstall
+add_task(async function test_uninstall() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) {
+ let addon = await promiseAddonByID(RESTARTLESS_ID);
+ isnot(addon, null, "Found add-on for uninstall");
+
+ addon.uninstall();
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: RESTARTLESS_ID, event: "onUninstalling" },
+ { id: RESTARTLESS_ID, event: "onUninstalled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected uninstall events");
+ });
+});
+
+// Test cancel of uninstall.
+add_task(async function test_cancel() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) {
+ let addon = await promiseAddonByID(CANCEL_ID);
+ isnot(addon, null, "Found add-on for cancelling uninstall");
+
+ addon.uninstall();
+
+ let events = await getListenerEvents(browser);
+ let expected = [{ id: CANCEL_ID, event: "onUninstalling" }];
+ Assert.deepEqual(events, expected, "Got expected uninstalling event");
+
+ addon.cancelUninstall();
+ events = await getListenerEvents(browser);
+ expected.push({ id: CANCEL_ID, event: "onOperationCancelled" });
+ Assert.deepEqual(events, expected, "Got expected cancel event");
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
new file mode 100644
index 0000000000..25989bf797
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
@@ -0,0 +1,63 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+async function getListenerEvents(browser) {
+ let result = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split("\n").map(JSON.parse);
+}
+
+const ID = "test@tests.mozilla.org";
+
+let provider = new MockProvider();
+provider.createAddons([
+ {
+ id: ID,
+ name: "Test add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+]);
+
+// Test disable and enable from content
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) {
+ let addon = await promiseAddonByID(ID);
+ isnot(addon, null, "Test addon exists");
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // Disable the addon from content.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return content.navigator.mozAddonManager
+ .getAddonByID("test@tests.mozilla.org")
+ .then(addon => addon.setEnabled(false));
+ });
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: ID, event: "onDisabling" },
+ { id: ID, event: "onDisabled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable events");
+
+ // Enable the addon from content.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return content.navigator.mozAddonManager
+ .getAddonByID("test@tests.mozilla.org")
+ .then(addon => addon.setEnabled(true));
+ });
+
+ events = await getListenerEvents(browser);
+ expected = expected.concat([
+ { id: ID, event: "onEnabling" },
+ { id: ID, event: "onEnabled" },
+ ]);
+ Assert.deepEqual(events, expected, "Got expected enable events");
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
new file mode 100644
index 0000000000..24d34c3f4d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
@@ -0,0 +1,652 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+const TESTPATH = "webapi_checkavailable.html";
+const TESTPAGE = `${SECURE_TESTROOT}${TESTPATH}`;
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org";
+
+const XPI_SHA =
+ "sha256:91121ed2c27f670f2307b9aebdd30979f147318c7fb9111c254c14ddbb84e4b0";
+
+const ID = "amosigned-xpi@tests.mozilla.org";
+// eh, would be good to just stat the real file instead of this...
+const XPI_LEN = 4287;
+
+AddonTestUtils.initMochitest(this);
+
+function waitForClear() {
+ const MSG = "WebAPICleanup";
+ return new Promise(resolve => {
+ let listener = {
+ receiveMessage(msg) {
+ if (msg.name == MSG) {
+ Services.mm.removeMessageListener(MSG, listener);
+ resolve();
+ }
+ },
+ };
+
+ Services.mm.addMessageListener(MSG, listener, true);
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+ info("added preferences");
+});
+
+// Wrapper around a common task to run in the content process to test
+// the mozAddonManager API. Takes a URL for the XPI to install and an
+// array of steps, each of which can either be an action to take
+// (i.e., start or cancel the install) or an install event to wait for.
+// Steps that look for a specific event may also include a "props" property
+// with properties that the AddonInstall object is expected to have when
+// that event is triggered.
+async function testInstall(browser, args, steps, description) {
+ let success = await SpecialPowers.spawn(
+ browser,
+ [{ args, steps }],
+ async function (opts) {
+ let { args, steps } = opts;
+ let install = await content.navigator.mozAddonManager.createInstall(args);
+ if (!install) {
+ await Promise.reject(
+ "createInstall() did not return an install object"
+ );
+ }
+
+ // Check that the initial state of the AddonInstall is sane.
+ if (install.state != "STATE_AVAILABLE") {
+ await Promise.reject("new install should be in STATE_AVAILABLE");
+ }
+ if (install.error != null) {
+ await Promise.reject("new install should have null error");
+ }
+
+ const events = [
+ "onDownloadStarted",
+ "onDownloadProgress",
+ "onDownloadEnded",
+ "onDownloadCancelled",
+ "onDownloadFailed",
+ "onInstallStarted",
+ "onInstallEnded",
+ "onInstallCancelled",
+ "onInstallFailed",
+ ];
+ let eventWaiter = null;
+ let receivedEvents = [];
+ let prevEvent = null;
+ events.forEach(event => {
+ install.addEventListener(event, e => {
+ receivedEvents.push({
+ event,
+ state: install.state,
+ error: install.error,
+ progress: install.progress,
+ maxProgress: install.maxProgress,
+ });
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+ });
+
+ // Returns a promise that is resolved when the given event occurs
+ // or rejects if a different event comes first or if props is supplied
+ // and properties on the AddonInstall don't match those in props.
+ function expectEvent(event, props) {
+ return new Promise((resolve, reject) => {
+ function check() {
+ let received = receivedEvents.shift();
+ // Skip any repeated onDownloadProgress events.
+ while (
+ received &&
+ received.event == prevEvent &&
+ prevEvent == "onDownloadProgress"
+ ) {
+ received = receivedEvents.shift();
+ }
+ // Wait for more events if we skipped all there were.
+ if (!received) {
+ eventWaiter = () => {
+ eventWaiter = null;
+ check();
+ };
+ return;
+ }
+ prevEvent = received.event;
+ if (received.event != event) {
+ let err = new Error(
+ `expected ${event} but got ${received.event}`
+ );
+ reject(err);
+ }
+ if (props) {
+ for (let key of Object.keys(props)) {
+ if (received[key] != props[key]) {
+ throw new Error(
+ `AddonInstall property ${key} was ${received[key]} but expected ${props[key]}`
+ );
+ }
+ }
+ }
+ resolve();
+ }
+ check();
+ });
+ }
+
+ while (steps.length) {
+ let nextStep = steps.shift();
+ if (nextStep.action) {
+ if (nextStep.action == "install") {
+ try {
+ await install.install();
+ if (nextStep.expectError) {
+ throw new Error("Expected install to fail but it did not");
+ }
+ } catch (err) {
+ if (!nextStep.expectError) {
+ throw new Error("Install failed unexpectedly");
+ }
+ }
+ } else if (nextStep.action == "cancel") {
+ await install.cancel();
+ } else {
+ throw new Error(`unknown action ${nextStep.action}`);
+ }
+ } else {
+ await expectEvent(nextStep.event, nextStep.props);
+ }
+ }
+
+ return true;
+ }
+ );
+
+ is(success, true, description);
+}
+
+function makeInstallTest(task) {
+ return async function () {
+ // withNewTab() will close the test tab before returning, at which point
+ // the cleanup event will come from the content process. We need to see
+ // that event but don't want to race to install a listener for it after
+ // the tab is closed. So set up the listener now but don't yield the
+ // listening promise until below.
+ let clearPromise = waitForClear();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, task);
+
+ await clearPromise;
+ is(AddonManager.webAPI.installs.size, 0, "AddonInstall was cleaned up");
+ };
+}
+
+function makeRegularTest(options, what) {
+ return makeInstallTest(async function (browser) {
+ let steps = [
+ { action: "install" },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ {
+ event: "onDownloadProgress",
+ props: { maxProgress: XPI_LEN },
+ },
+ {
+ event: "onDownloadEnded",
+ props: {
+ state: "STATE_DOWNLOADED",
+ progress: XPI_LEN,
+ maxProgress: XPI_LEN,
+ },
+ },
+ {
+ event: "onInstallStarted",
+ props: { state: "STATE_INSTALLING" },
+ },
+ {
+ event: "onInstallEnded",
+ props: { state: "STATE_INSTALLED" },
+ },
+ ];
+
+ let installPromptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ let promptPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ options.addonId
+ );
+
+ await testInstall(browser, options, steps, what);
+
+ await installPromptPromise;
+
+ await promptPromise;
+
+ // Sanity check to ensure that the test in makeInstallTest() that
+ // installs.size == 0 means we actually did clean up.
+ Assert.greater(
+ AddonManager.webAPI.installs.size,
+ 0,
+ "webAPI is tracking the AddonInstall"
+ );
+
+ let addon = await promiseAddonByID(ID);
+ isnot(addon, null, "Found the addon");
+
+ // Check that the expected installTelemetryInfo has been stored in the addon details.
+ AddonTestUtils.checkInstallInfo(addon, {
+ method: "amWebAPI",
+ source: "test-host",
+ sourceURL: /https:\/\/example.com\/.*\/webapi_checkavailable.html/,
+ });
+
+ await addon.uninstall();
+
+ addon = await promiseAddonByID(ID);
+ is(addon, null, "Addon was uninstalled");
+ });
+}
+
+let addonId = XPI_ADDON_ID;
+add_task(makeRegularTest({ url: XPI_URL, addonId }, "a basic install works"));
+add_task(
+ makeRegularTest(
+ { url: XPI_URL, addonId, hash: null },
+ "install with hash=null works"
+ )
+);
+add_task(
+ makeRegularTest(
+ { url: XPI_URL, addonId, hash: "" },
+ "install with empty string for hash works"
+ )
+);
+add_task(
+ makeRegularTest(
+ { url: XPI_URL, addonId, hash: XPI_SHA },
+ "install with hash works"
+ )
+);
+
+add_task(
+ makeInstallTest(async function (browser) {
+ let steps = [
+ { action: "cancel" },
+ {
+ event: "onDownloadCancelled",
+ props: {
+ state: "STATE_CANCELLED",
+ error: null,
+ },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL },
+ steps,
+ "canceling an install works"
+ );
+
+ let addons = await promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ Assert.greater(
+ AddonManager.webAPI.installs.size,
+ 0,
+ "webAPI is tracking the AddonInstall"
+ );
+ })
+);
+
+add_task(
+ makeInstallTest(async function (browser) {
+ let steps = [
+ { action: "install", expectError: true },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ { event: "onDownloadProgress" },
+ {
+ event: "onDownloadFailed",
+ props: {
+ state: "STATE_DOWNLOAD_FAILED",
+ error: "ERROR_NETWORK_FAILURE",
+ },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL + "bogus" },
+ steps,
+ "install of a bad url fails"
+ );
+
+ let addons = await promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ Assert.greater(
+ AddonManager.webAPI.installs.size,
+ 0,
+ "webAPI is tracking the AddonInstall"
+ );
+ })
+);
+
+add_task(
+ makeInstallTest(async function (browser) {
+ let steps = [
+ { action: "install", expectError: true },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ { event: "onDownloadProgress" },
+ {
+ event: "onDownloadFailed",
+ props: {
+ state: "STATE_DOWNLOAD_FAILED",
+ error: "ERROR_INCORRECT_HASH",
+ },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL, hash: "sha256:bogus" },
+ steps,
+ "install with bad hash fails"
+ );
+
+ let addons = await promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ Assert.greater(
+ AddonManager.webAPI.installs.size,
+ 0,
+ "webAPI is tracking the AddonInstall"
+ );
+ })
+);
+
+add_task(async function test_permissions_and_policy() {
+ async function testBadUrl(url, pattern, successMessage) {
+ gBrowser.selectedTab = await BrowserTestUtils.addTab(gBrowser, TESTPAGE);
+ let browser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ await BrowserTestUtils.browserLoaded(browser);
+ let result = await SpecialPowers.spawn(
+ browser,
+ [{ url, pattern }],
+ function (opts) {
+ return new Promise(resolve => {
+ content.navigator.mozAddonManager
+ .createInstall({ url: opts.url })
+ .then(
+ () => {
+ resolve({
+ success: false,
+ message: "createInstall should not have succeeded",
+ });
+ },
+ err => {
+ if (err.message.match(new RegExp(opts.pattern))) {
+ resolve({ success: true });
+ }
+ resolve({
+ success: false,
+ message: `Wrong error message: ${err.message}`,
+ });
+ }
+ );
+ });
+ }
+ );
+ is(result.success, true, result.message || successMessage);
+ }
+
+ await testBadUrl(
+ "i am not a url",
+ "NS_ERROR_MALFORMED_URI",
+ "Installing from an unparseable URL fails"
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ let popupPromise = promisePopupNotificationShown(
+ "addon-install-webapi-blocked"
+ );
+ await Promise.all([
+ testBadUrl(
+ "https://addons.not-really-mozilla.org/impostor.xpi",
+ "not permitted",
+ "Installing from non-approved URL fails"
+ ),
+ popupPromise,
+ ]);
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ const blocked_install_message = "Custom Policy Block Message";
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "*": {
+ install_sources: [],
+ blocked_install_message,
+ },
+ },
+ },
+ });
+
+ popupPromise = promisePopupNotificationShown("addon-install-policy-blocked");
+
+ await testBadUrl(
+ XPI_URL,
+ "not permitted by policy",
+ "Installing from policy blocked origin fails"
+ );
+
+ const panel = await popupPromise;
+ const description = panel.querySelector(
+ ".popup-notification-description"
+ ).textContent;
+ ok(
+ description.startsWith("Your organization"),
+ "Policy specific error is shown."
+ );
+ ok(
+ description.endsWith(` ${blocked_install_message}`),
+ `Found the expected custom blocked message in "${description}"`
+ );
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ ExtensionSettings: {
+ "*": {
+ install_sources: ["<all_urls>"],
+ },
+ },
+ },
+ });
+});
+
+add_task(
+ makeInstallTest(async function (browser) {
+ let xpiURL = `${SECURE_TESTROOT}../xpinstall/incompatible.xpi`;
+ let id = "incompatible-xpi@tests.mozilla.org";
+
+ let steps = [
+ { action: "install", expectError: true },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ { event: "onDownloadProgress" },
+ { event: "onDownloadEnded" },
+ { event: "onDownloadCancelled", error: "ERROR_INCOMPATIBLE" },
+ ];
+
+ await testInstall(
+ browser,
+ { url: xpiURL },
+ steps,
+ "install of an incompatible XPI fails"
+ );
+
+ let addons = await promiseAddonsByIDs([id]);
+ is(addons[0], null, "The addon was not installed");
+ })
+);
+
+add_task(
+ makeInstallTest(async function (browser) {
+ let id = "amosigned-xpi@tests.mozilla.org";
+ let version = "2.1";
+
+ await AddonTestUtils.loadBlocklistRawData({
+ extensionsMLBF: [
+ {
+ stash: { blocked: [`${id}:${version}`], unblocked: [] },
+ stash_time: 0,
+ },
+ ],
+ });
+
+ let steps = [
+ { action: "install", expectError: true },
+ { event: "onDownloadStarted" },
+ { event: "onDownloadProgress" },
+ { event: "onDownloadEnded" },
+ {
+ event: "onDownloadCancelled",
+ props: { state: "STATE_CANCELLED", error: "ERROR_BLOCKLISTED" },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL },
+ steps,
+ "install of a blocked XPI fails"
+ );
+
+ let addons = await promiseAddonsByIDs([id]);
+ is(addons[0], null, "The addon was not installed");
+
+ // Clear the blocklist.
+ await AddonTestUtils.loadBlocklistRawData({
+ extensionsMLBF: [
+ {
+ stash: { blocked: [], unblocked: [] },
+ stash_time: 0,
+ },
+ ],
+ });
+ })
+);
+
+add_task(
+ makeInstallTest(async function (browser) {
+ const options = { url: XPI_URL, addonId };
+ let steps = [
+ { action: "install" },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ {
+ event: "onDownloadProgress",
+ props: { maxProgress: XPI_LEN },
+ },
+ {
+ event: "onDownloadEnded",
+ props: {
+ state: "STATE_DOWNLOADED",
+ progress: XPI_LEN,
+ maxProgress: XPI_LEN,
+ },
+ },
+ {
+ event: "onInstallStarted",
+ props: { state: "STATE_INSTALLING" },
+ },
+ {
+ event: "onInstallEnded",
+ props: { state: "STATE_INSTALLED" },
+ },
+ ];
+
+ await SpecialPowers.spawn(browser, [TESTPATH], testPath => {
+ // `sourceURL` should match the exact location, even after a location
+ // update using the history API. In this case, we update the URL with
+ // query parameters and expect `sourceURL` to contain those parameters.
+ content.history.pushState(
+ {}, // state
+ "", // title
+ `/${testPath}?some=query&par=am`
+ );
+ });
+
+ let installPromptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ let promptPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ options.addonId
+ );
+
+ await Promise.all([
+ testInstall(browser, options, steps, "install to check source URL"),
+ installPromptPromise,
+ promptPromise,
+ ]);
+
+ let addon = await promiseAddonByID(ID);
+
+ registerCleanupFunction(async () => {
+ await addon.uninstall();
+ });
+
+ // Check that the expected installTelemetryInfo has been stored in the
+ // addon details.
+ AddonTestUtils.checkInstallInfo(addon, {
+ method: "amWebAPI",
+ source: "test-host",
+ sourceURL:
+ "https://example.com/webapi_checkavailable.html?some=query&par=am",
+ });
+ })
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js
new file mode 100644
index 0000000000..5bc291fe7a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js
@@ -0,0 +1,60 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+
+function waitForClear() {
+ const MSG = "WebAPICleanup";
+ return new Promise(resolve => {
+ let listener = {
+ receiveMessage(msg) {
+ if (msg.name == MSG) {
+ Services.mm.removeMessageListener(MSG, listener);
+ resolve();
+ }
+ },
+ };
+
+ Services.mm.addMessageListener(MSG, listener, true);
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["xpinstall.enabled", false],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+ info("added preferences");
+});
+
+async function testInstall(browser, args) {
+ let success = await SpecialPowers.spawn(
+ browser,
+ [{ args }],
+ async function (opts) {
+ let { args } = opts;
+ let install;
+ try {
+ install = await content.navigator.mozAddonManager.createInstall(args);
+ } catch (e) {}
+ return !!install;
+ }
+ );
+ is(success, false, "Install was blocked");
+}
+
+add_task(async function () {
+ // withNewTab() will close the test tab before returning, at which point
+ // the cleanup event will come from the content process. We need to see
+ // that event but don't want to race to install a listener for it after
+ // the tab is closed. So set up the listener now but don't yield the
+ // listening promise until below.
+ let clearPromise = waitForClear();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) {
+ await testInstall(browser, { url: XPI_URL });
+ });
+
+ await clearPromise;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js
new file mode 100644
index 0000000000..dd1df90907
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js
@@ -0,0 +1,79 @@
+"use strict";
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const URL = `${SECURE_TESTROOT}addons/browser_theme.xpi`;
+
+add_task(async function test_theme_install() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ let updates = [];
+ function observer(subject, topic, data) {
+ updates.push(JSON.stringify(subject.wrappedJSObject));
+ }
+ Services.obs.addObserver(observer, "lightweight-theme-styling-update");
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(observer, "lightweight-theme-styling-update");
+ });
+
+ let sawConfirm = false;
+ promisePopupNotificationShown("addon-install-confirmation").then(panel => {
+ sawConfirm = true;
+ panel.button.click();
+ });
+
+ let prompt1 = waitAppMenuNotificationShown(
+ "addon-installed",
+ "theme@tests.mozilla.org",
+ false
+ );
+ let installPromise = SpecialPowers.spawn(browser, [URL], async url => {
+ let install = await content.navigator.mozAddonManager.createInstall({
+ url,
+ });
+ return install.install();
+ });
+ await prompt1;
+
+ ok(sawConfirm, "Confirm notification was displayed before installation");
+
+ // Open a new window and test the app menu panel from there. This verifies the
+ // incognito checkbox as well as finishing install in this case.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await waitAppMenuNotificationShown(
+ "addon-installed",
+ "theme@tests.mozilla.org",
+ true,
+ newWin
+ );
+ await installPromise;
+ ok(true, "Theme install completed");
+
+ await BrowserTestUtils.closeWindow(newWin);
+
+ Assert.equal(updates.length, 1, "Got a single theme update");
+ let parsed = JSON.parse(updates[0]);
+ ok(
+ parsed.theme.headerURL.endsWith("/testImage.png"),
+ "Theme update has the expected headerURL"
+ );
+ is(
+ parsed.theme.id,
+ "theme@tests.mozilla.org",
+ "Theme update includes the theme ID"
+ );
+ is(
+ parsed.theme.version,
+ "1.0",
+ "Theme update includes the theme's version"
+ );
+
+ let addon = await AddonManager.getAddonByID(parsed.theme.id);
+ await addon.uninstall();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
new file mode 100644
index 0000000000..ad4afe0fa7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+function testWithAPI(task) {
+ return async function () {
+ await BrowserTestUtils.withNewTab(TESTPAGE, task);
+ };
+}
+
+function API_uninstallByID(browser, id) {
+ return SpecialPowers.spawn(browser, [id], async function (id) {
+ let addon = await content.navigator.mozAddonManager.getAddonByID(id);
+
+ let result = await addon.uninstall();
+ return result;
+ });
+}
+
+add_task(
+ testWithAPI(async function (browser) {
+ const ID1 = "addon1@tests.mozilla.org";
+ const ID2 = "addon2@tests.mozilla.org";
+ const ID3 = "addon3@tests.mozilla.org";
+
+ let provider = new MockProvider();
+
+ provider.addAddon(new MockAddon(ID1, "Test add-on 1", "extension", 0));
+ provider.addAddon(new MockAddon(ID2, "Test add-on 2", "extension", 0));
+
+ let addon = new MockAddon(ID3, "Test add-on 3", "extension", 0);
+ addon.permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+ provider.addAddon(addon);
+
+ let [a1, a2, a3] = await promiseAddonsByIDs([ID1, ID2, ID3]);
+ isnot(a1, null, "addon1 is installed");
+ isnot(a2, null, "addon2 is installed");
+ isnot(a3, null, "addon3 is installed");
+
+ let result = await API_uninstallByID(browser, ID1);
+ is(result, true, "uninstall of addon1 succeeded");
+
+ [a1, a2, a3] = await promiseAddonsByIDs([ID1, ID2, ID3]);
+ is(a1, null, "addon1 is uninstalled");
+ isnot(a2, null, "addon2 is still installed");
+
+ result = await API_uninstallByID(browser, ID2);
+ is(result, true, "uninstall of addon2 succeeded");
+ [a2] = await promiseAddonsByIDs([ID2]);
+ is(a2, null, "addon2 is uninstalled");
+
+ await Assert.rejects(
+ API_uninstallByID(browser, ID3),
+ /Addon cannot be uninstalled/,
+ "Unable to uninstall addon"
+ );
+
+ // Cleanup addon3
+ a3.permissions |= AddonManager.PERM_CAN_UNINSTALL;
+ await a3.uninstall();
+ [a3] = await promiseAddonsByIDs([ID3]);
+ is(a3, null, "addon3 is uninstalled");
+ })
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js b/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js
new file mode 100644
index 0000000000..123fe0c665
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js
@@ -0,0 +1,82 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function pngArrayBuffer(size) {
+ const canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.height = canvas.width = size;
+ const ctx = canvas.getContext("2d");
+ ctx.fillStyle = "blue";
+ ctx.fillRect(0, 0, size, size);
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ const fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+}
+
+async function checkIconInView(view, name, findIcon) {
+ const manager = await open_manager(view);
+ const icon = findIcon(manager.document);
+ const size = Number(icon.src.match(/icon(\d+)\.png/)[1]);
+ is(
+ icon.clientWidth,
+ icon.clientHeight,
+ `The icon should be square in ${name}`
+ );
+ is(
+ size,
+ icon.clientWidth * window.devicePixelRatio,
+ `The correct icon size should have been chosen in ${name}`
+ );
+ await close_manager(manager);
+}
+
+add_task(async function test_addon_icon() {
+ // This test loads an extension with a variety of icon sizes, and checks that the
+ // fitting one is chosen. If this fails it's because you changed the icon size in
+ // about:addons but didn't update some AddonManager.getPreferredIconURL call.
+ const id = "@test-addon-icon";
+ const icons = {};
+ const files = {};
+ const file = await pngArrayBuffer(256);
+ for (let size = 1; size <= 256; ++size) {
+ let fileName = `icon${size}.png`;
+ icons[size] = fileName;
+ files[fileName] = file;
+ }
+ const extensionDefinition = {
+ useAddonManager: "temporary",
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ icons,
+ },
+ files,
+ };
+
+ const extension = ExtensionTestUtils.loadExtension(extensionDefinition);
+ await extension.startup();
+
+ await checkIconInView("addons://list/extension", "list", doc => {
+ return getAddonCard(doc.defaultView, id).querySelector(".addon-icon");
+ });
+
+ await checkIconInView(
+ "addons://detail/" + encodeURIComponent(id),
+ "details",
+ doc => {
+ return getAddonCard(doc.defaultView, id).querySelector(".addon-icon");
+ }
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
new file mode 100644
index 0000000000..9180bbcf91
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
@@ -0,0 +1,593 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+);
+
+var gManagerWindow;
+
+AddonTestUtils.initMochitest(this);
+
+function get_test_items() {
+ var items = {};
+
+ for (let item of gManagerWindow.document.querySelectorAll("addon-card")) {
+ items[item.getAttribute("addon-id")] = item;
+ }
+
+ return items;
+}
+
+function getHtmlElem(selector) {
+ return gManagerWindow.document.querySelector(selector);
+}
+
+function getPrivateBrowsingBadge(card) {
+ return card.querySelector(".addon-badge-private-browsing-allowed");
+}
+
+function getPreferencesButtonAtListView(card) {
+ return card.querySelector("panel-item[action='preferences']");
+}
+
+function getPreferencesButtonAtDetailsView() {
+ return getHtmlElem("panel-item[action='preferences']");
+}
+
+function isInlineOptionsVisible() {
+ // The following button is used to open the inline options browser.
+ return !getHtmlElem(".tab-button[name='preferences']").hidden;
+}
+
+function getPrivateBrowsingValue() {
+ return getHtmlElem("input[type='radio'][name='private-browsing']:checked")
+ .value;
+}
+
+async function setPrivateBrowsingValue(value, id) {
+ let changePromise = new Promise(resolve => {
+ const listener = (type, { extensionId, added, removed }) => {
+ if (extensionId == id) {
+ // Let's make sure we received the right message
+ let { permissions } = value == "0" ? removed : added;
+ ok(permissions.includes("internal:privateBrowsingAllowed"));
+ Management.off("change-permissions", listener);
+ resolve();
+ }
+ };
+ Management.on("change-permissions", listener);
+ });
+ let radio = getHtmlElem(
+ `input[type="radio"][name="private-browsing"][value="${value}"]`
+ );
+ // NOTE: not using EventUtils.synthesizeMouseAtCenter here because it
+ // does make this test to fail intermittently in some jobs (e.g. TV jobs)
+ radio.click();
+ // Let's make sure we wait until the change has peristed in the database
+ return changePromise;
+}
+
+// Check whether the private browsing inputs are visible in the details view.
+function checkIsModifiable(expected) {
+ if (expected) {
+ is_element_visible(
+ getHtmlElem(".addon-detail-row-private-browsing"),
+ "Private browsing should be visible"
+ );
+ } else {
+ is_element_hidden(
+ getHtmlElem(".addon-detail-row-private-browsing"),
+ "Private browsing should be hidden"
+ );
+ }
+ checkHelpRow(".addon-detail-row-private-browsing", expected);
+}
+
+// Check whether the details view shows that private browsing is forcibly disallowed.
+function checkIsDisallowed(expected) {
+ if (expected) {
+ is_element_visible(
+ getHtmlElem(".addon-detail-row-private-browsing-disallowed"),
+ "Private browsing should be disallowed"
+ );
+ } else {
+ is_element_hidden(
+ getHtmlElem(".addon-detail-row-private-browsing-disallowed"),
+ "Private browsing should not be disallowed"
+ );
+ }
+ checkHelpRow(".addon-detail-row-private-browsing-disallowed", expected);
+}
+
+// Check whether the details view shows that private browsing is forcibly allowed.
+function checkIsRequired(expected) {
+ if (expected) {
+ is_element_visible(
+ getHtmlElem(".addon-detail-row-private-browsing-required"),
+ "Private browsing should be required"
+ );
+ } else {
+ is_element_hidden(
+ getHtmlElem(".addon-detail-row-private-browsing-required"),
+ "Private browsing should not be required"
+ );
+ }
+ checkHelpRow(".addon-detail-row-private-browsing-required", expected);
+}
+
+function checkHelpRow(selector, expected) {
+ let helpRow = getHtmlElem(`${selector} + .addon-detail-help-row`);
+ if (expected) {
+ is_element_visible(helpRow, `Help row should be shown: ${selector}`);
+ is_element_visible(helpRow.querySelector("a"), "Expected learn more link");
+ } else {
+ is_element_hidden(helpRow, `Help row should be hidden: ${selector}`);
+ }
+}
+
+async function hasPrivateAllowed(id) {
+ let perms = await ExtensionPermissions.get(id);
+ return perms.permissions.includes("internal:privateBrowsingAllowed");
+}
+
+add_task(async function test_badge_and_toggle_incognito() {
+ let addons = new Map([
+ [
+ "@test-default",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@test-default" },
+ },
+ },
+ },
+ ],
+ [
+ "@test-override",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@test-override" },
+ },
+ },
+ incognitoOverride: "spanning",
+ },
+ ],
+ [
+ "@test-override-permanent",
+ {
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@test-override-permanent" },
+ },
+ },
+ incognitoOverride: "spanning",
+ },
+ ],
+ [
+ "@test-not-allowed",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "@test-not-allowed" },
+ },
+ incognito: "not_allowed",
+ },
+ },
+ ],
+ ]);
+ let extensions = [];
+ for (let definition of addons.values()) {
+ let extension = ExtensionTestUtils.loadExtension(definition);
+ extensions.push(extension);
+ await extension.startup();
+ }
+
+ gManagerWindow = await open_manager("addons://list/extension");
+ let items = get_test_items();
+ for (let [id, definition] of addons.entries()) {
+ ok(items[id], `${id} listed`);
+ let badge = getPrivateBrowsingBadge(items[id]);
+ if (definition.incognitoOverride == "spanning") {
+ is_element_visible(badge, `private browsing badge is visible`);
+ } else {
+ is_element_hidden(badge, `private browsing badge is hidden`);
+ }
+ }
+ await close_manager(gManagerWindow);
+
+ for (let [id, definition] of addons.entries()) {
+ gManagerWindow = await open_manager(
+ "addons://detail/" + encodeURIComponent(id)
+ );
+ ok(true, `==== ${id} detail opened`);
+ if (definition.manifest.incognito == "not_allowed") {
+ checkIsModifiable(false);
+ ok(!(await hasPrivateAllowed(id)), "Private browsing permission not set");
+ checkIsDisallowed(true);
+ } else {
+ // This assumes PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS, we test other options in a later test in this file.
+ checkIsModifiable(true);
+ if (definition.incognitoOverride == "spanning") {
+ is(getPrivateBrowsingValue(), "1", "Private browsing should be on");
+ ok(await hasPrivateAllowed(id), "Private browsing permission set");
+ await setPrivateBrowsingValue("0", id);
+ is(getPrivateBrowsingValue(), "0", "Private browsing should be off");
+ ok(
+ !(await hasPrivateAllowed(id)),
+ "Private browsing permission removed"
+ );
+ } else {
+ is(getPrivateBrowsingValue(), "0", "Private browsing should be off");
+ ok(
+ !(await hasPrivateAllowed(id)),
+ "Private browsing permission not set"
+ );
+ await setPrivateBrowsingValue("1", id);
+ is(getPrivateBrowsingValue(), "1", "Private browsing should be on");
+ ok(await hasPrivateAllowed(id), "Private browsing permission set");
+ }
+ }
+ await close_manager(gManagerWindow);
+ }
+
+ for (let extension of extensions) {
+ await extension.unload();
+ }
+});
+
+add_task(async function test_addon_preferences_button() {
+ let addons = new Map([
+ [
+ "test-inline-options@mozilla.com",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension with inline options",
+ browser_specific_settings: {
+ gecko: { id: "test-inline-options@mozilla.com" },
+ },
+ options_ui: { page: "options.html", open_in_tab: false },
+ },
+ },
+ ],
+ [
+ "test-newtab-options@mozilla.com",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension with options page in a new tab",
+ browser_specific_settings: {
+ gecko: { id: "test-newtab-options@mozilla.com" },
+ },
+ options_ui: { page: "options.html", open_in_tab: true },
+ },
+ },
+ ],
+ [
+ "test-not-allowed@mozilla.com",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension not allowed in PB windows",
+ incognito: "not_allowed",
+ browser_specific_settings: {
+ gecko: { id: "test-not-allowed@mozilla.com" },
+ },
+ options_ui: { page: "options.html", open_in_tab: true },
+ },
+ },
+ ],
+ ]);
+
+ async function runTest(openInPrivateWin) {
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ private: openInPrivateWin,
+ });
+
+ gManagerWindow = await open_manager(
+ "addons://list/extension",
+ undefined,
+ undefined,
+ undefined,
+ win
+ );
+
+ const checkPrefsVisibility = (id, hasInlinePrefs, expectVisible) => {
+ if (!hasInlinePrefs) {
+ const detailsPrefBtn = getPreferencesButtonAtDetailsView();
+ is(
+ !detailsPrefBtn.hidden,
+ expectVisible,
+ `The ${id} prefs button in the addon details has the expected visibility`
+ );
+ } else {
+ is(
+ isInlineOptionsVisible(),
+ expectVisible,
+ `The ${id} inline prefs in the addon details has the expected visibility`
+ );
+ }
+ };
+
+ const setAddonPrivateBrowsingAccess = async (id, allowPrivateBrowsing) => {
+ const cardUpdatedPromise = BrowserTestUtils.waitForEvent(
+ getHtmlElem("addon-card"),
+ "update"
+ );
+ is(
+ getPrivateBrowsingValue(),
+ allowPrivateBrowsing ? "0" : "1",
+ `Private browsing should be initially ${
+ allowPrivateBrowsing ? "off" : "on"
+ }`
+ );
+
+ // Get the DOM element we want to click on (to allow or disallow the
+ // addon on private browsing windows).
+ await setPrivateBrowsingValue(allowPrivateBrowsing ? "1" : "0", id);
+
+ info(`Waiting for details view of ${id} to be reloaded`);
+ await cardUpdatedPromise;
+
+ is(
+ getPrivateBrowsingValue(),
+ allowPrivateBrowsing ? "1" : "0",
+ `Private browsing should be initially ${
+ allowPrivateBrowsing ? "on" : "off"
+ }`
+ );
+
+ is(
+ await hasPrivateAllowed(id),
+ allowPrivateBrowsing,
+ `Private browsing permission ${
+ allowPrivateBrowsing ? "added" : "removed"
+ }`
+ );
+ let badge = getPrivateBrowsingBadge(getHtmlElem("addon-card"));
+ is(
+ !badge.hidden,
+ allowPrivateBrowsing,
+ `Expected private browsing badge at ${id}`
+ );
+ };
+
+ const extensions = [];
+ for (const definition of addons.values()) {
+ const extension = ExtensionTestUtils.loadExtension(definition);
+ extensions.push(extension);
+ await extension.startup();
+ }
+
+ const items = get_test_items();
+
+ for (const id of addons.keys()) {
+ // Check the preferences button in the addon list page.
+ is(
+ getPreferencesButtonAtListView(items[id]).hidden,
+ openInPrivateWin,
+ `The ${id} prefs button in the addon list has the expected visibility`
+ );
+ }
+
+ for (const [id, definition] of addons.entries()) {
+ // Check the preferences button or inline frame in the addon
+ // details page.
+ info(`Opening addon details for ${id}`);
+ const hasInlinePrefs = !definition.manifest.options_ui.open_in_tab;
+ const onceViewChanged = wait_for_view_load(gManagerWindow, null, true);
+ gManagerWindow.loadView(`addons://detail/${encodeURIComponent(id)}`);
+ await onceViewChanged;
+
+ checkPrefsVisibility(id, hasInlinePrefs, !openInPrivateWin);
+
+ // While testing in a private window, also check that the preferences
+ // are going to be visible when we toggle the PB access for the addon.
+ if (openInPrivateWin && definition.manifest.incognito !== "not_allowed") {
+ await setAddonPrivateBrowsingAccess(id, true);
+ checkPrefsVisibility(id, hasInlinePrefs, true);
+
+ await setAddonPrivateBrowsingAccess(id, false);
+ checkPrefsVisibility(id, hasInlinePrefs, false);
+ }
+ }
+
+ for (const extension of extensions) {
+ await extension.unload();
+ }
+
+ await close_manager(gManagerWindow);
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ // run tests in private and non-private windows.
+ await runTest(true);
+ await runTest(false);
+});
+
+add_task(async function test_addon_postinstall_incognito_hidden_checkbox() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.langpacks.signatures.required", false]],
+ });
+
+ const TEST_ADDONS = [
+ {
+ manifest: {
+ name: "Extension incognito default opt-in",
+ browser_specific_settings: {
+ gecko: { id: "ext-incognito-default-opt-in@mozilla.com" },
+ },
+ },
+ },
+ {
+ manifest: {
+ name: "Extension incognito not_allowed",
+ browser_specific_settings: {
+ gecko: { id: "ext-incognito-not-allowed@mozilla.com" },
+ },
+ incognito: "not_allowed",
+ },
+ },
+ {
+ manifest: {
+ name: "Static Theme",
+ browser_specific_settings: {
+ gecko: { id: "static-theme@mozilla.com" },
+ },
+ theme: {
+ colors: {
+ frame: "#FFFFFF",
+ tab_background_text: "#000",
+ },
+ },
+ },
+ },
+ {
+ manifest: {
+ name: "Dictionary",
+ browser_specific_settings: { gecko: { id: "dictionary@mozilla.com" } },
+ dictionaries: {
+ und: "dictionaries/und.dic",
+ },
+ },
+ files: {
+ "dictionaries/und.dic": "",
+ "dictionaries/und.aff": "",
+ },
+ },
+ {
+ manifest: {
+ name: "Langpack",
+ browser_specific_settings: { gecko: { id: "langpack@mozilla.com" } },
+ langpack_id: "und",
+ languages: {
+ und: {
+ chrome_resources: {
+ global: "chrome/und/locale/und/global",
+ },
+ version: "20190326174300",
+ },
+ },
+ },
+ },
+ ];
+
+ for (let definition of TEST_ADDONS) {
+ let { id } = definition.manifest.browser_specific_settings.gecko;
+ info(
+ `Testing incognito checkbox visibility on ${id} post install notification`
+ );
+
+ const xpi = AddonTestUtils.createTempWebExtensionFile(definition);
+ let install = await AddonManager.getInstallForFile(xpi);
+
+ await Promise.all([
+ waitAppMenuNotificationShown("addon-installed", id, true),
+ install.install().then(() => {
+ Services.obs.notifyObservers(
+ {
+ addon: install.addon,
+ target: gBrowser.selectedBrowser,
+ },
+ "webextension-install-notify"
+ );
+ }),
+ ]);
+
+ const { addon } = install;
+ const { permissions } = addon;
+ const canChangePBAccess = Boolean(
+ permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
+ );
+
+ if (id === "ext-incognito-default-opt-in@mozilla.com") {
+ ok(
+ canChangePBAccess,
+ `${id} should have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission`
+ );
+ } else {
+ ok(
+ !canChangePBAccess,
+ `${id} should not have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission`
+ );
+ }
+
+ // This tests the visibility of various private detail rows.
+ gManagerWindow = await open_manager(
+ "addons://detail/" + encodeURIComponent(id)
+ );
+ info(`addon ${id} detail opened`);
+ if (addon.type === "extension") {
+ checkIsModifiable(canChangePBAccess);
+ let required = addon.incognito === "spanning";
+ checkIsRequired(!canChangePBAccess && required);
+ checkIsDisallowed(!canChangePBAccess && !required);
+ } else {
+ checkIsModifiable(false);
+ checkIsRequired(false);
+ checkIsDisallowed(false);
+ }
+ await close_manager(gManagerWindow);
+
+ await addon.uninstall();
+ }
+
+ // It is not possible to create a privileged add-on and install it, so just
+ // simulate an installed privileged add-on and check the UI.
+ await test_incognito_of_privileged_addons();
+});
+
+// Checks that the private browsing flag of privileged add-ons cannot be modified.
+async function test_incognito_of_privileged_addons() {
+ // In mochitests it is not possible to create and install a privileged add-on
+ // or a system add-on, so create a mock provider that simulates privileged
+ // add-ons (which lack the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission).
+ let provider = new MockProvider();
+ provider.createAddons([
+ {
+ name: "default incognito",
+ id: "default-incognito@mock",
+ incognito: "spanning", // This is the default.
+ // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission.
+ permissions: 0,
+ },
+ {
+ name: "not_allowed incognito",
+ id: "not-allowed-incognito@mock",
+ incognito: "not_allowed",
+ // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission.
+ permissions: 0,
+ },
+ ]);
+
+ gManagerWindow = await open_manager(
+ "addons://detail/default-incognito%40mock"
+ );
+ checkIsModifiable(false);
+ checkIsRequired(true);
+ checkIsDisallowed(false);
+ await close_manager(gManagerWindow);
+
+ gManagerWindow = await open_manager(
+ "addons://detail/not-allowed-incognito%40mock"
+ );
+ checkIsModifiable(false);
+ checkIsRequired(false);
+ checkIsDisallowed(true);
+ await close_manager(gManagerWindow);
+
+ provider.unregister();
+}
diff --git a/toolkit/mozapps/extensions/test/browser/discovery/api_response.json b/toolkit/mozapps/extensions/test/browser/discovery/api_response.json
new file mode 100644
index 0000000000..b36d3c1f02
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery/api_response.json
@@ -0,0 +1,679 @@
+{
+ "results": [
+ {
+ "description_text": "",
+ "addon": {
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid": "{e0d2e13b-2e07-49d5-9574-eb0227482320}",
+ "authors": [
+ {
+ "id": 7804538,
+ "name": "Sondergaard",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/7/7804/7804538.png?modified=1392125542",
+ "username": "EatingStick",
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/7804538/"
+ }
+ ],
+ "previews": [
+ {
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183758.png?modified=1555593109",
+ "image_size": [680, 92],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183758.png?modified=1555593109",
+ "id": 183758,
+ "thumbnail_size": [473, 64],
+ "caption": null
+ },
+ {
+ "id": 183768,
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183768.png?modified=1555593111",
+ "image_size": [760, 92],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183768.png?modified=1555593111",
+ "caption": null,
+ "thumbnail_size": [529, 64]
+ },
+ {
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183777.png?modified=1555593112",
+ "id": 183777,
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183777.png?modified=1555593112",
+ "image_size": [720, 92],
+ "caption": null,
+ "thumbnail_size": [501, 64]
+ }
+ ],
+ "name": "Tigers Matter ** DON'T DELTE ME**",
+ "id": 496012,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/tigers-matter/",
+ "type": "statictheme",
+ "ratings": {
+ "average": 4.7636,
+ "text_count": 55,
+ "count": 55,
+ "bayesian_average": 4.75672
+ },
+ "slug": "tigers-matter",
+ "average_daily_users": 1,
+ "current_version": {
+ "compatibility": {
+ "firefox": {
+ "max": "*",
+ "min": "53.0"
+ },
+ "android": {
+ "max": "*",
+ "min": "65.0"
+ }
+ },
+ "is_strict_compatibility_enabled": false,
+ "id": 1655900,
+ "files": [
+ {
+ "is_restart_required": false,
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/376561/tigers_matter_dont_delte_me-2.0-an+fx.xpi?src=",
+ "created": "2019-04-18T13:11:48Z",
+ "size": 86337,
+ "status": "public",
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "permissions": [],
+ "hash": "sha256:ebeb6e4f40ceafbc4affc5bc9a182ed44ae410d71b8c5f9c547f8d45863e0c37",
+ "platform": "all",
+ "id": 376561
+ }
+ ]
+ }
+ },
+ "is_recommendation": false
+ },
+ {
+ "is_recommendation": false,
+ "addon": {
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/awesome-screenshot-plus-/",
+ "type": "extension",
+ "ratings": {
+ "count": 848,
+ "bayesian_average": 3.87925,
+ "average": 3.8797,
+ "text_count": 842
+ },
+ "slug": "awesome-screenshot-plus-",
+ "average_daily_users": 1,
+ "current_version": {
+ "is_strict_compatibility_enabled": false,
+ "id": 1532816,
+ "files": [
+ {
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/253549/awesome_screenshot_plus-7-an+fx.xpi?src=",
+ "is_restart_required": false,
+ "size": 4196,
+ "created": "2017-09-01T13:31:17Z",
+ "is_webextension": true,
+ "status": "public",
+ "is_mozilla_signed_extension": false,
+ "permissions": [],
+ "hash": "sha256:4cd8e9b7e89f61e6855d01c73c5c05920c1e0e91f3ae0f45adbb4bd9919f59d7",
+ "platform": "all",
+ "id": 253549
+ }
+ ],
+ "compatibility": {
+ "android": {
+ "min": "48.0",
+ "max": "*"
+ },
+ "firefox": {
+ "max": "*",
+ "min": "48.0"
+ }
+ }
+ },
+ "authors": [
+ {
+ "username": "diigo-inc",
+ "name": "Diigo Inc.",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/0/6/6724.png?modified=1554393597",
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/6724/",
+ "id": 6724
+ }
+ ],
+ "icon_url": "https://addons-dev-cdn.allizom.org/user-media/addon_icons/287/287841-64.png?modified=mcrushed",
+ "guid": "jid0-GXjLLfbCoAx0LcltEdFrEkQdQPI@jetpack",
+ "previews": [
+ {
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54638.png?modified=1543388383",
+ "id": 54638,
+ "image_size": [625, 525],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54638.png?modified=1543388383",
+ "caption": "Capture and annotate a page",
+ "thumbnail_size": [571, 480]
+ },
+ {
+ "caption": "Crop selected area",
+ "thumbnail_size": [571, 480],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54639.png?modified=1543388385",
+ "image_size": [625, 525],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54639.png?modified=1543388385",
+ "id": 54639
+ },
+ {
+ "caption": "Save as a local file or upload to get a sharable link",
+ "thumbnail_size": [640, 234],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54641.png?modified=1543388385",
+ "image_size": [700, 256],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54641.png?modified=1543388385",
+ "id": 54641
+ }
+ ],
+ "name": "Awesome Screenshot Plus - Capture, Annotate & More",
+ "id": 287841
+ },
+ "description_text": "Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines and text, blur sensitive info, one-click upload to share. And more! Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines"
+ },
+ {
+ "description_text": "Help Admins in their daily work",
+ "addon": {
+ "slug": "amo-admin-assistant-test",
+ "average_daily_users": 0,
+ "current_version": {
+ "files": [
+ {
+ "is_restart_required": false,
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/255370/amo_admin_assistant-4.2-fx.xpi?src=",
+ "size": 16016,
+ "created": "2018-08-21T16:49:21Z",
+ "is_webextension": true,
+ "status": "public",
+ "is_mozilla_signed_extension": false,
+ "permissions": [
+ "tabs",
+ "https://addons-internal.prod.mozaws.net/*"
+ ],
+ "hash": "sha256:cd28c841a6daf8a2e3c94b0773b373fec0213404b70074309326cfc75e6725d3",
+ "platform": "all",
+ "id": 255370
+ }
+ ],
+ "is_strict_compatibility_enabled": false,
+ "id": 1534709,
+ "compatibility": {
+ "firefox": {
+ "min": "45.0",
+ "max": "*"
+ }
+ }
+ },
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/amo-admin-assistant-test/",
+ "ratings": {
+ "bayesian_average": 0,
+ "count": 0,
+ "text_count": 0,
+ "average": 0
+ },
+ "type": "extension",
+ "id": 496168,
+ "guid": "aaa-test-icon@xulforge.com",
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "authors": [
+ {
+ "id": 4230,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/4230/",
+ "username": "jorge-villalobos",
+ "name": "Jorge Villalobos",
+ "picture_url": null
+ }
+ ],
+ "previews": [],
+ "name": "AMO Admin Assistant Test"
+ },
+ "is_recommendation": false
+ },
+ {
+ "addon": {
+ "authors": [
+ {
+ "name": "LexaDev",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10640/10640485.png?modified=1554812253",
+ "username": "LexaSV",
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/10640485/",
+ "id": 10640485
+ }
+ ],
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid": "{f9b9cdd3-91ae-476e-9c21-d5ecfce9889f}",
+ "previews": [
+ {
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183694.png?modified=1555593096",
+ "image_size": [680, 92],
+ "id": 183694,
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183694.png?modified=1555593096",
+ "thumbnail_size": [473, 64],
+ "caption": null
+ },
+ {
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183699.png?modified=1555593097",
+ "id": 183699,
+ "image_size": [760, 92],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183699.png?modified=1555593097",
+ "caption": null,
+ "thumbnail_size": [529, 64]
+ },
+ {
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183703.png?modified=1555593098",
+ "image_size": [720, 92],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183703.png?modified=1555593098",
+ "id": 183703,
+ "caption": null,
+ "thumbnail_size": [501, 64]
+ }
+ ],
+ "name": "iarba",
+ "id": 495969,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/iarba/",
+ "ratings": {
+ "bayesian_average": 4.86128,
+ "count": 10,
+ "text_count": 10,
+ "average": 4.9
+ },
+ "type": "statictheme",
+ "slug": "iarba",
+ "current_version": {
+ "files": [
+ {
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/376535/iarba-2.0-an+fx.xpi?src=",
+ "is_restart_required": false,
+ "size": 895804,
+ "created": "2019-04-18T13:11:35Z",
+ "is_mozilla_signed_extension": false,
+ "status": "public",
+ "is_webextension": true,
+ "id": 376535,
+ "permissions": [],
+ "platform": "all",
+ "hash": "sha256:d7ecbdfa8ba56c5d08129c867e68b02ffc8c6000a7f7f85d85d2a558045babfa"
+ }
+ ],
+ "is_strict_compatibility_enabled": false,
+ "id": 1655874,
+ "compatibility": {
+ "android": {
+ "min": "65.0",
+ "max": "*"
+ },
+ "firefox": {
+ "min": "53.0",
+ "max": "*"
+ }
+ }
+ },
+ "average_daily_users": 1
+ },
+ "description_text": "",
+ "is_recommendation": false
+ },
+ {
+ "description_text": "Get international weather forecasts",
+ "addon": {
+ "id": 502855,
+ "authors": [
+ {
+ "id": 10641527,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/10641527/",
+ "name": "Amoga-dev",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641527.png?modified=1555333028",
+ "username": "Amoga_dev_REST"
+ }
+ ],
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid": "forecastfox@s3_fix_version",
+ "previews": [],
+ "name": "Forecastfox (fix version)",
+ "slug": "forecastfox-fix-version",
+ "current_version": {
+ "id": 1541667,
+ "is_strict_compatibility_enabled": false,
+ "files": [
+ {
+ "permissions": [
+ "activeTab",
+ "tabs",
+ "background",
+ "storage",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://www.s3blog.org/geolocation.html*",
+ "https://embed.windy.com/embed2.html*"
+ ],
+ "platform": "all",
+ "hash": "sha256:89e4de4ce86005c57b0197f671e86936aaf843ebd5751caae02cad4991ccbf0a",
+ "id": 262328,
+ "is_webextension": true,
+ "status": "public",
+ "is_mozilla_signed_extension": false,
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/262328/forecastfox_fix_version-4.20-an+fx.xpi?src=",
+ "is_restart_required": false,
+ "created": "2019-01-16T07:54:26Z",
+ "size": 1331686
+ }
+ ],
+ "compatibility": {
+ "android": {
+ "min": "51.0",
+ "max": "*"
+ },
+ "firefox": {
+ "min": "51.0",
+ "max": "*"
+ }
+ }
+ },
+ "average_daily_users": 0,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/forecastfox-fix-version/",
+ "type": "extension",
+ "ratings": {
+ "count": 0,
+ "bayesian_average": 0,
+ "average": 0,
+ "text_count": 0
+ }
+ },
+ "is_recommendation": false
+ },
+ {
+ "description_text": "A test extension from webext-generator.",
+ "addon": {
+ "name": "tabby cat",
+ "previews": [],
+ "guid": "{1ed4b641-bac7-4492-b304-6ddc01f538ae}",
+ "icon_url": "https://addons-dev-cdn.allizom.org/user-media/addon_icons/502/502774-64.png?modified=f289a992",
+ "authors": [
+ {
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/10641572/",
+ "username": "AdminUserTestDev1",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641572.png?modified=1555675110",
+ "name": "úþÿ Ψ Φ ֎",
+ "id": 10641572
+ }
+ ],
+ "id": 502774,
+ "ratings": {
+ "bayesian_average": 0,
+ "count": 0,
+ "text_count": 0,
+ "average": 0
+ },
+ "type": "extension",
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/tabby-catextension/",
+ "current_version": {
+ "compatibility": {
+ "firefox": {
+ "max": "*",
+ "min": "48.0"
+ },
+ "android": {
+ "max": "*",
+ "min": "48.0"
+ }
+ },
+ "is_strict_compatibility_enabled": false,
+ "id": 1541570,
+ "files": [
+ {
+ "created": "2018-12-04T09:54:24Z",
+ "size": 4374,
+ "is_restart_required": false,
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/262231/tabby_cat-1.0-an+fx.xpi?src=",
+ "is_mozilla_signed_extension": false,
+ "status": "public",
+ "is_webextension": true,
+ "id": 262231,
+ "hash": "sha256:f12c8a8b71e7d4c48e38db6b6a374ca8dcde42d6cb13fb1f2a8299bb51116615",
+ "platform": "all",
+ "permissions": []
+ }
+ ]
+ },
+ "average_daily_users": 1,
+ "slug": "tabby-catextension"
+ },
+ "is_recommendation": false
+ },
+ {
+ "addon": {
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/the-moon-cat/",
+ "ratings": {
+ "average": 4.8182,
+ "text_count": 11,
+ "count": 11,
+ "bayesian_average": 4.78325
+ },
+ "type": "statictheme",
+ "slug": "the-moon-cat",
+ "average_daily_users": 2,
+ "current_version": {
+ "files": [
+ {
+ "is_mozilla_signed_extension": false,
+ "status": "public",
+ "is_webextension": true,
+ "id": 262333,
+ "permissions": [],
+ "hash": "sha256:d159190add69c739b0fe07b19ad3ff48045c5ded502a8df0f892b8feb645c5ae",
+ "platform": "all",
+ "is_restart_required": false,
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/262333/the_moon_cat-1.0-an+fx.xpi?src=",
+ "size": 102889,
+ "created": "2019-01-16T08:31:21Z"
+ }
+ ],
+ "is_strict_compatibility_enabled": false,
+ "id": 1541672,
+ "compatibility": {
+ "firefox": {
+ "max": "*",
+ "min": "53.0"
+ },
+ "android": {
+ "min": "65.0",
+ "max": "*"
+ }
+ }
+ },
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "authors": [
+ {
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/5822165/",
+ "username": "Rallara",
+ "name": "Rallara",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/5/5822/5822165.png?modified=1391855104",
+ "id": 5822165
+ }
+ ],
+ "guid": "{db4f6548-da04-43fb-a03e-249bf70ef5a1}",
+ "previews": [
+ {
+ "thumbnail_size": [473, 64],
+ "caption": null,
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14307.png?modified=1547627485",
+ "image_size": [680, 92],
+ "id": 14307,
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14307.png?modified=1547627485"
+ },
+ {
+ "thumbnail_size": [529, 64],
+ "caption": null,
+ "id": 14308,
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14308.png?modified=1547627486",
+ "image_size": [760, 92],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14308.png?modified=1547627486"
+ },
+ {
+ "thumbnail_size": [501, 64],
+ "caption": null,
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14309.png?modified=1547627487",
+ "image_size": [720, 92],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14309.png?modified=1547627487",
+ "id": 14309
+ }
+ ],
+ "name": "the Moon Cat",
+ "id": 502859
+ },
+ "description_text": "",
+ "is_recommendation": false
+ },
+ {
+ "is_recommendation": false,
+ "addon": {
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid": "{2e5ff8c8-32fe-46d0-9fc8-6b8986621f3c}",
+ "authors": [
+ {
+ "id": 10641570,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/10641570/",
+ "name": "BobsDisplayName",
+ "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641570.png?modified=1536063975",
+ "username": "BobsUserName"
+ }
+ ],
+ "previews": [],
+ "name": "SI",
+ "id": 495710,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/search_by_image/",
+ "ratings": {
+ "average": 3.8333,
+ "text_count": 5,
+ "count": 6,
+ "bayesian_average": 3.77144
+ },
+ "type": "extension",
+ "slug": "search_by_image",
+ "current_version": {
+ "files": [
+ {
+ "id": 262271,
+ "permissions": [
+ "contextMenus",
+ "storage",
+ "tabs",
+ "activeTab",
+ "notifications",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://*/*",
+ "https://*/*",
+ "ftp://*/*",
+ "file:///*"
+ ],
+ "platform": "all",
+ "hash": "sha256:f358b24d0b950f5acf035342dec64c99ee2e22a5cf369e7c787ebb00013127a8",
+ "is_mozilla_signed_extension": false,
+ "is_webextension": true,
+ "status": "public",
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/262271/search_by_image_reverse_image_search-1.12.6-fx.xpi?src=",
+ "is_restart_required": false,
+ "size": 372225,
+ "created": "2018-12-14T13:48:23Z"
+ }
+ ],
+ "id": 1541610,
+ "is_strict_compatibility_enabled": false,
+ "compatibility": {
+ "firefox": {
+ "min": "57.0",
+ "max": "*"
+ }
+ }
+ },
+ "average_daily_users": 374
+ },
+ "description_text": "AAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGG"
+ },
+ {
+ "addon": {
+ "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid": "{f5e7a6ee-ebe0-4add-8f75-b5e4015feca1}",
+ "authors": [
+ {
+ "id": 8733220,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/user/8733220/",
+ "username": "michellet-2",
+ "name": "michellet",
+ "picture_url": null
+ }
+ ],
+ "previews": [
+ {
+ "caption": null,
+ "thumbnail_size": [473, 64],
+ "id": 14304,
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14304.png?modified=1547627480",
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14304.png?modified=1547627480",
+ "image_size": [680, 92]
+ },
+ {
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14305.png?modified=1547627481",
+ "image_size": [760, 92],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14305.png?modified=1547627481",
+ "id": 14305,
+ "thumbnail_size": [529, 64],
+ "caption": null
+ },
+ {
+ "caption": null,
+ "thumbnail_size": [501, 64],
+ "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14306.png?modified=1547627482",
+ "id": 14306,
+ "image_size": [720, 92],
+ "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14306.png?modified=1547627482"
+ }
+ ],
+ "name": "Purple Sparkles",
+ "id": 502858,
+ "url": "https://addons-dev.allizom.org/en-US/firefox/addon/purple-sparkles/",
+ "type": "statictheme",
+ "ratings": {
+ "count": 4,
+ "bayesian_average": 4.1476,
+ "average": 4.25,
+ "text_count": 3
+ },
+ "slug": "purple-sparkles",
+ "average_daily_users": 445,
+ "current_version": {
+ "compatibility": {
+ "firefox": {
+ "min": "53.0",
+ "max": "*"
+ },
+ "android": {
+ "max": "*",
+ "min": "65.0"
+ }
+ },
+ "id": 1541671,
+ "is_strict_compatibility_enabled": false,
+ "files": [
+ {
+ "created": "2019-01-16T08:31:18Z",
+ "size": 237348,
+ "url": "https://addons-dev.allizom.org/firefox/downloads/file/262332/purple_sparkles-1.0-an+fx.xpi?src=",
+ "is_restart_required": false,
+ "is_mozilla_signed_extension": false,
+ "is_webextension": true,
+ "status": "public",
+ "id": 262332,
+ "hash": "sha256:5a3d311b7c1be2ee32446dbcf1422c5d7c786c5a237aa3d4e2939074ab50ad30",
+ "platform": "all",
+ "permissions": []
+ }
+ ]
+ }
+ },
+ "description_text": "",
+ "is_recommendation": false
+ }
+ ],
+ "count": 9
+}
diff --git a/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json b/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json
new file mode 100644
index 0000000000..a5a3af7835
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json
@@ -0,0 +1 @@
+{ "results": [] }
diff --git a/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png
new file mode 100644
index 0000000000..862d1dd10c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/head.js b/toolkit/mozapps/extensions/test/browser/head.js
new file mode 100644
index 0000000000..482429177c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -0,0 +1,1714 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* globals end_test */
+
+/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+let { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+var pathParts = gTestPath.split("/");
+// Drop the test filename
+pathParts.splice(pathParts.length - 1, pathParts.length);
+
+const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";
+
+const TESTROOT = "http://example.com/" + RELATIVE_DIR;
+const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR;
+const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
+const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR;
+const CHROMEROOT = pathParts.join("/") + "/";
+const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
+const PREF_XPI_ENABLED = "xpinstall.enabled";
+const PREF_UPDATEURL = "extensions.update.url";
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
+
+const MANAGER_URI = "about:addons";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const PREF_STRICT_COMPAT = "extensions.strictCompatibility";
+
+var PREF_CHECK_COMPATIBILITY;
+(function () {
+ var channel = Services.prefs.getCharPref("app.update.channel", "default");
+ if (
+ channel != "aurora" &&
+ channel != "beta" &&
+ channel != "release" &&
+ channel != "esr"
+ ) {
+ var version = "nightly";
+ } else {
+ version = Services.appinfo.version.replace(
+ /^([^\.]+\.[0-9]+[a-z]*).*/gi,
+ "$1"
+ );
+ }
+ PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version;
+})();
+
+var gPendingTests = [];
+var gTestsRun = 0;
+var gTestStart = null;
+
+var gRestorePrefs = [
+ { name: PREF_LOGGING_ENABLED },
+ { name: "extensions.webservice.discoverURL" },
+ { name: "extensions.update.url" },
+ { name: "extensions.update.background.url" },
+ { name: "extensions.update.enabled" },
+ { name: "extensions.update.autoUpdateDefault" },
+ { name: "extensions.getAddons.get.url" },
+ { name: "extensions.getAddons.getWithPerformance.url" },
+ { name: "extensions.getAddons.cache.enabled" },
+ { name: "devtools.chrome.enabled" },
+ { name: PREF_STRICT_COMPAT },
+ { name: PREF_CHECK_COMPATIBILITY },
+];
+
+for (let pref of gRestorePrefs) {
+ if (!Services.prefs.prefHasUserValue(pref.name)) {
+ pref.type = "clear";
+ continue;
+ }
+ pref.type = Services.prefs.getPrefType(pref.name);
+ if (pref.type == Services.prefs.PREF_BOOL) {
+ pref.value = Services.prefs.getBoolPref(pref.name);
+ } else if (pref.type == Services.prefs.PREF_INT) {
+ pref.value = Services.prefs.getIntPref(pref.name);
+ } else if (pref.type == Services.prefs.PREF_STRING) {
+ pref.value = Services.prefs.getCharPref(pref.name);
+ }
+}
+
+// Turn logging on for all tests
+Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
+
+function promiseFocus(window) {
+ return new Promise(resolve => waitForFocus(resolve, window));
+}
+
+// Tools to disable and re-enable the background update and blocklist timers
+// so that tests can protect themselves from unwanted timer events.
+var gCatMan = Services.catMan;
+// Default value from toolkit/mozapps/extensions/extensions.manifest, but disable*UpdateTimer()
+// records the actual value so we can put it back in enable*UpdateTimer()
+var backgroundUpdateConfig =
+ "@mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400";
+
+var UTIMER = "update-timer";
+var AMANAGER = "addonManager";
+var BLOCKLIST = "nsBlocklistService";
+
+function disableBackgroundUpdateTimer() {
+ info("Disabling " + UTIMER + " " + AMANAGER);
+ backgroundUpdateConfig = gCatMan.getCategoryEntry(UTIMER, AMANAGER);
+ gCatMan.deleteCategoryEntry(UTIMER, AMANAGER, true);
+}
+
+function enableBackgroundUpdateTimer() {
+ info("Enabling " + UTIMER + " " + AMANAGER);
+ gCatMan.addCategoryEntry(
+ UTIMER,
+ AMANAGER,
+ backgroundUpdateConfig,
+ false,
+ true
+ );
+}
+
+registerCleanupFunction(function () {
+ // Restore prefs
+ for (let pref of gRestorePrefs) {
+ if (pref.type == "clear") {
+ Services.prefs.clearUserPref(pref.name);
+ } else if (pref.type == Services.prefs.PREF_BOOL) {
+ Services.prefs.setBoolPref(pref.name, pref.value);
+ } else if (pref.type == Services.prefs.PREF_INT) {
+ Services.prefs.setIntPref(pref.name, pref.value);
+ } else if (pref.type == Services.prefs.PREF_STRING) {
+ Services.prefs.setCharPref(pref.name, pref.value);
+ }
+ }
+
+ return AddonManager.getAllInstalls().then(aInstalls => {
+ for (let install of aInstalls) {
+ if (install instanceof MockInstall) {
+ continue;
+ }
+
+ ok(
+ false,
+ "Should not have seen an install of " +
+ install.sourceURI.spec +
+ " in state " +
+ install.state
+ );
+ install.cancel();
+ }
+ });
+});
+
+function log_exceptions(aCallback, ...aArgs) {
+ try {
+ return aCallback.apply(null, aArgs);
+ } catch (e) {
+ info("Exception thrown: " + e);
+ throw e;
+ }
+}
+
+function log_callback(aPromise, aCallback) {
+ aPromise.then(aCallback).catch(e => info("Exception thrown: " + e));
+ return aPromise;
+}
+
+function add_test(test) {
+ gPendingTests.push(test);
+}
+
+function run_next_test() {
+ // Make sure we're not calling run_next_test from inside an add_task() test
+ // We're inside the browser_test.js 'testScope' here
+ if (this.__tasks) {
+ throw new Error(
+ "run_next_test() called from an add_task() test function. " +
+ "run_next_test() should not be called from inside add_task() " +
+ "under any circumstances!"
+ );
+ }
+ if (gTestsRun > 0) {
+ info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms");
+ }
+
+ if (!gPendingTests.length) {
+ executeSoon(end_test);
+ return;
+ }
+
+ gTestsRun++;
+ var test = gPendingTests.shift();
+ if (test.name) {
+ info("Running test " + gTestsRun + " (" + test.name + ")");
+ } else {
+ info("Running test " + gTestsRun);
+ }
+
+ gTestStart = Date.now();
+ executeSoon(() => log_exceptions(test));
+}
+
+var get_tooltip_info = async function (addonEl, managerWindow) {
+ // Extract from title attribute.
+ const { addon } = addonEl;
+ const name = addon.name;
+
+ let nameWithVersion = addonEl.addonNameEl.title;
+ if (addonEl.addon.userDisabled) {
+ // TODO - Bug 1558077: Currently Fluent is clearing the addon title
+ // when the addon is disabled, fixing it requires changes to the
+ // HTML about:addons localized strings, and then remove this
+ // workaround.
+ nameWithVersion = `${name} ${addon.version}`;
+ }
+
+ return {
+ name,
+ version: nameWithVersion.substring(name.length + 1),
+ };
+};
+
+function get_addon_file_url(aFilename) {
+ try {
+ var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ var fileurl = cr.convertChromeURL(
+ makeURI(CHROMEROOT + "addons/" + aFilename)
+ );
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+ } catch (ex) {
+ var jar = getJar(CHROMEROOT + "addons/" + aFilename);
+ var tmpDir = extractJarToTmp(jar);
+ tmpDir.append(aFilename);
+
+ return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL);
+ }
+}
+
+function check_all_in_list(aManager, aIds, aIgnoreExtras) {
+ var doc = aManager.document;
+ var list = doc.getElementById("addon-list");
+
+ var inlist = [];
+ var node = list.firstChild;
+ while (node) {
+ if (node.value) {
+ inlist.push(node.value);
+ }
+ node = node.nextSibling;
+ }
+
+ for (let id of aIds) {
+ if (!inlist.includes(id)) {
+ ok(false, "Should find " + id + " in the list");
+ }
+ }
+
+ if (aIgnoreExtras) {
+ return;
+ }
+
+ for (let inlistItem of inlist) {
+ if (!aIds.includes(inlistItem)) {
+ ok(false, "Shouldn't have seen " + inlistItem + " in the list");
+ }
+ }
+}
+
+function getAddonCard(win, id) {
+ return win.document.querySelector(`addon-card[addon-id="${id}"]`);
+}
+
+async function wait_for_view_load(
+ aManagerWindow,
+ aCallback,
+ aForceWait,
+ aLongerTimeout
+) {
+ // Wait one tick to make sure that the microtask related to an
+ // async loadView call originated from outsite about:addons
+ // is already executing (otherwise isLoading would be still false
+ // and we wouldn't be waiting for that load before resolving
+ // the promise returned by this test helper function).
+ await Promise.resolve();
+
+ let p = new Promise(resolve => {
+ requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
+
+ if (!aForceWait && !aManagerWindow.gViewController.isLoading) {
+ resolve(aManagerWindow);
+ return;
+ }
+
+ aManagerWindow.document.addEventListener(
+ "view-loaded",
+ function () {
+ resolve(aManagerWindow);
+ },
+ { once: true }
+ );
+ });
+
+ return log_callback(p, aCallback);
+}
+
+function wait_for_manager_load(aManagerWindow, aCallback) {
+ info("Waiting for initialization");
+ return log_callback(
+ aManagerWindow.promiseInitialized.then(() => aManagerWindow),
+ aCallback
+ );
+}
+
+function open_manager(
+ aView,
+ aCallback,
+ aLoadCallback,
+ aLongerTimeout,
+ aWin = window
+) {
+ let p = new Promise((resolve, reject) => {
+ async function setup_manager(aManagerWindow) {
+ if (aLoadCallback) {
+ log_exceptions(aLoadCallback, aManagerWindow);
+ }
+
+ if (aView) {
+ aManagerWindow.loadView(aView);
+ }
+
+ Assert.notEqual(
+ aManagerWindow,
+ null,
+ "Should have an add-ons manager window"
+ );
+ is(
+ aManagerWindow.location.href,
+ MANAGER_URI,
+ "Should be displaying the correct UI"
+ );
+
+ await promiseFocus(aManagerWindow);
+ info("window has focus, waiting for manager load");
+ await wait_for_manager_load(aManagerWindow);
+ info("Manager waiting for view load");
+ await wait_for_view_load(aManagerWindow, null, null, aLongerTimeout);
+ resolve(aManagerWindow);
+ }
+
+ info("Loading manager window in tab");
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ if (aSubject.location.href != MANAGER_URI) {
+ info("Ignoring load event for " + aSubject.location.href);
+ return;
+ }
+ setup_manager(aSubject);
+ }, "EM-loaded");
+
+ aWin.gBrowser.selectedTab = BrowserTestUtils.addTab(aWin.gBrowser);
+ aWin.switchToTabHavingURI(MANAGER_URI, true, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ });
+
+ // The promise resolves with the manager window, so it is passed to the callback
+ return log_callback(p, aCallback);
+}
+
+function close_manager(aManagerWindow, aCallback, aLongerTimeout) {
+ let p = new Promise((resolve, reject) => {
+ requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
+
+ Assert.notEqual(
+ aManagerWindow,
+ null,
+ "Should have an add-ons manager window to close"
+ );
+ is(
+ aManagerWindow.location.href,
+ MANAGER_URI,
+ "Should be closing window with correct URI"
+ );
+
+ aManagerWindow.addEventListener("unload", function listener() {
+ try {
+ dump("Manager window unload handler\n");
+ this.removeEventListener("unload", listener);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+
+ info("Telling manager window to close");
+ aManagerWindow.close();
+ info("Manager window close() call returned");
+
+ return log_callback(p, aCallback);
+}
+
+function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) {
+ if (!aManagerWindow) {
+ return open_manager(aView, aCallback, aLoadCallback);
+ }
+
+ return close_manager(aManagerWindow).then(() =>
+ open_manager(aView, aCallback, aLoadCallback)
+ );
+}
+
+function wait_for_window_open(aCallback) {
+ let p = new Promise(resolve => {
+ Services.wm.addListener({
+ onOpenWindow(aXulWin) {
+ Services.wm.removeListener(this);
+
+ let domwindow = aXulWin.docShell.domWindow;
+ domwindow.addEventListener(
+ "load",
+ function () {
+ executeSoon(function () {
+ resolve(domwindow);
+ });
+ },
+ { once: true }
+ );
+ },
+
+ onCloseWindow(aWindow) {},
+ });
+ });
+
+ return log_callback(p, aCallback);
+}
+
+function formatDate(aDate) {
+ const dtOptions = { year: "numeric", month: "long", day: "numeric" };
+ return aDate.toLocaleDateString(undefined, dtOptions);
+}
+
+function is_hidden(aElement) {
+ var style = aElement.ownerGlobal.getComputedStyle(aElement);
+ if (style.display == "none") {
+ return true;
+ }
+ if (style.visibility != "visible") {
+ return true;
+ }
+
+ // Hiding a parent element will hide all its children
+ if (aElement.parentNode != aElement.ownerDocument) {
+ return is_hidden(aElement.parentNode);
+ }
+
+ return false;
+}
+
+function is_element_visible(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(!is_hidden(aElement), aMsg || aElement + " should be visible");
+}
+
+function is_element_hidden(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(aElement), aMsg || aElement + " should be hidden");
+}
+
+function promiseAddonByID(aId) {
+ return AddonManager.getAddonByID(aId);
+}
+
+function promiseAddonsByIDs(aIDs) {
+ return AddonManager.getAddonsByIDs(aIDs);
+}
+/**
+ * Install an add-on and call a callback when complete.
+ *
+ * The callback will receive the Addon for the installed add-on.
+ */
+async function install_addon(path, cb, pathPrefix = TESTROOT) {
+ let install = await AddonManager.getInstallForURL(pathPrefix + path);
+ let p = new Promise((resolve, reject) => {
+ install.addListener({
+ onInstallEnded: () => resolve(install.addon),
+ });
+
+ install.install();
+ });
+
+ return log_callback(p, cb);
+}
+
+function CategoryUtilities(aManagerWindow) {
+ this.window = aManagerWindow;
+ this.window.addEventListener("unload", () => (this.window = null), {
+ once: true,
+ });
+}
+
+CategoryUtilities.prototype = {
+ window: null,
+
+ get _categoriesBox() {
+ return this.window.document.querySelector("categories-box");
+ },
+
+ getSelectedViewId() {
+ let selectedItem = this._categoriesBox.querySelector("[selected]");
+ isnot(selectedItem, null, "A category should be selected");
+ return selectedItem.getAttribute("viewid");
+ },
+
+ get selectedCategory() {
+ isnot(
+ this.window,
+ null,
+ "Should not get selected category when manager window is not loaded"
+ );
+ let viewId = this.getSelectedViewId();
+ let view = this.window.gViewController.parseViewId(viewId);
+ return view.type == "list" ? view.param : view.type;
+ },
+
+ get(categoryType) {
+ isnot(
+ this.window,
+ null,
+ "Should not get category when manager window is not loaded"
+ );
+
+ let button = this._categoriesBox.querySelector(`[name="${categoryType}"]`);
+ if (button) {
+ return button;
+ }
+
+ ok(false, "Should have found a category with type " + categoryType);
+ return null;
+ },
+
+ isVisible(categoryButton) {
+ isnot(
+ this.window,
+ null,
+ "Should not check visible state when manager window is not loaded"
+ );
+
+ // There are some tests checking this before the categories have loaded.
+ if (!categoryButton) {
+ return false;
+ }
+
+ if (categoryButton.disabled || categoryButton.hidden) {
+ return false;
+ }
+
+ return !is_hidden(categoryButton);
+ },
+
+ isTypeVisible(categoryType) {
+ return this.isVisible(this.get(categoryType));
+ },
+
+ open(categoryButton) {
+ isnot(
+ this.window,
+ null,
+ "Should not open category when manager window is not loaded"
+ );
+ ok(
+ this.isVisible(categoryButton),
+ "Category should be visible if attempting to open it"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(categoryButton, {}, this.window);
+
+ // Use wait_for_view_load until all open_manager calls are gone.
+ return wait_for_view_load(this.window);
+ },
+
+ openType(categoryType) {
+ return this.open(this.get(categoryType));
+ },
+};
+
+// Returns a promise that will resolve when the certificate error override has been added, or reject
+// if there is some failure.
+function addCertOverride(host) {
+ return new Promise((resolve, reject) => {
+ let req = new XMLHttpRequest();
+ req.open("GET", "https://" + host + "/");
+ req.onload = reject;
+ req.onerror = () => {
+ if (req.channel && req.channel.securityInfo) {
+ let securityInfo = req.channel.securityInfo;
+ if (securityInfo.serverCert) {
+ let cos = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+ cos.rememberValidityOverride(
+ host,
+ -1,
+ {},
+ securityInfo.serverCert,
+ false
+ );
+ resolve();
+ return;
+ }
+ }
+ reject();
+ };
+ req.send(null);
+ });
+}
+
+// Returns a promise that will resolve when the necessary certificate overrides have been added.
+function addCertOverrides() {
+ return Promise.all([
+ addCertOverride("nocert.example.com"),
+ addCertOverride("self-signed.example.com"),
+ addCertOverride("untrusted.example.com"),
+ addCertOverride("expired.example.com"),
+ ]);
+}
+
+/** *** Mock Provider *****/
+
+function MockProvider(addonTypes) {
+ this.addons = [];
+ this.installs = [];
+ this.addonTypes = addonTypes ?? ["extension"];
+
+ var self = this;
+ registerCleanupFunction(function () {
+ if (self.started) {
+ self.unregister();
+ }
+ });
+
+ this.register();
+}
+
+MockProvider.prototype = {
+ addons: null,
+ installs: null,
+ addonTypes: null,
+ started: null,
+ queryDelayPromise: Promise.resolve(),
+
+ blockQueryResponses() {
+ this.queryDelayPromise = new Promise(resolve => {
+ this._unblockQueries = resolve;
+ });
+ },
+
+ unblockQueryResponses() {
+ if (this._unblockQueries) {
+ this._unblockQueries();
+ this._unblockQueries = null;
+ } else {
+ throw new Error("Queries are not blocked");
+ }
+ },
+
+ /** *** Utility functions *****/
+
+ /**
+ * Register this provider with the AddonManager
+ */
+ register: function MP_register() {
+ info("Registering mock add-on provider");
+ // addonTypes is supposedly the full set of types supported by the provider.
+ // The current list is not complete (there are tests that mock add-on types
+ // other than "extension"), but it doesn't affect tests since addonTypes is
+ // mainly used to determine whether any of the AddonManager's providers
+ // support a type, and XPIProvider already defines the types of interest.
+ AddonManagerPrivate.registerProvider(this, this.addonTypes);
+ },
+
+ /**
+ * Unregister this provider with the AddonManager
+ */
+ unregister: function MP_unregister() {
+ info("Unregistering mock add-on provider");
+ AddonManagerPrivate.unregisterProvider(this);
+ },
+
+ /**
+ * Adds an add-on to the list of add-ons that this provider exposes to the
+ * AddonManager, dispatching appropriate events in the process.
+ *
+ * @param aAddon
+ * The add-on to add
+ */
+ addAddon: function MP_addAddon(aAddon) {
+ var oldAddons = this.addons.filter(aOldAddon => aOldAddon.id == aAddon.id);
+ var oldAddon = oldAddons.length ? oldAddons[0] : null;
+
+ this.addons = this.addons.filter(aOldAddon => aOldAddon.id != aAddon.id);
+
+ this.addons.push(aAddon);
+ aAddon._provider = this;
+
+ if (!this.started) {
+ return;
+ }
+
+ let requiresRestart =
+ (aAddon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL) !=
+ 0;
+ AddonManagerPrivate.callInstallListeners(
+ "onExternalInstall",
+ null,
+ aAddon,
+ oldAddon,
+ requiresRestart
+ );
+ },
+
+ /**
+ * Removes an add-on from the list of add-ons that this provider exposes to
+ * the AddonManager, dispatching the onUninstalled event in the process.
+ *
+ * @param aAddon
+ * The add-on to add
+ */
+ removeAddon: function MP_removeAddon(aAddon) {
+ var pos = this.addons.indexOf(aAddon);
+ if (pos == -1) {
+ ok(
+ false,
+ "Tried to remove an add-on that wasn't registered with the mock provider"
+ );
+ return;
+ }
+
+ this.addons.splice(pos, 1);
+
+ if (!this.started) {
+ return;
+ }
+
+ AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
+ },
+
+ /**
+ * Adds an add-on install to the list of installs that this provider exposes
+ * to the AddonManager, dispatching appropriate events in the process.
+ *
+ * @param aInstall
+ * The add-on install to add
+ */
+ addInstall: function MP_addInstall(aInstall) {
+ this.installs.push(aInstall);
+ aInstall._provider = this;
+
+ if (!this.started) {
+ return;
+ }
+
+ aInstall.callListeners("onNewInstall");
+ },
+
+ removeInstall: function MP_removeInstall(aInstall) {
+ var pos = this.installs.indexOf(aInstall);
+ if (pos == -1) {
+ ok(
+ false,
+ "Tried to remove an install that wasn't registered with the mock provider"
+ );
+ return;
+ }
+
+ this.installs.splice(pos, 1);
+ },
+
+ /**
+ * Creates a set of mock add-on objects and adds them to the list of add-ons
+ * managed by this provider.
+ *
+ * @param aAddonProperties
+ * An array of objects containing properties describing the add-ons
+ * @return Array of the new MockAddons
+ */
+ createAddons: function MP_createAddons(aAddonProperties) {
+ var newAddons = [];
+ for (let addonProp of aAddonProperties) {
+ let addon = new MockAddon(addonProp.id);
+ for (let prop in addonProp) {
+ if (prop == "id") {
+ continue;
+ }
+ if (prop == "applyBackgroundUpdates") {
+ addon._applyBackgroundUpdates = addonProp[prop];
+ } else if (prop == "appDisabled") {
+ addon._appDisabled = addonProp[prop];
+ } else if (prop == "userDisabled") {
+ addon.setUserDisabled(addonProp[prop]);
+ } else {
+ addon[prop] = addonProp[prop];
+ }
+ }
+ if (!addon.optionsType && !!addon.optionsURL) {
+ addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG;
+ }
+
+ // Make sure the active state matches the passed in properties
+ addon.isActive = addon.shouldBeActive;
+
+ this.addAddon(addon);
+ newAddons.push(addon);
+ }
+
+ return newAddons;
+ },
+
+ /**
+ * Creates a set of mock add-on install objects and adds them to the list
+ * of installs managed by this provider.
+ *
+ * @param aInstallProperties
+ * An array of objects containing properties describing the installs
+ * @return Array of the new MockInstalls
+ */
+ createInstalls: function MP_createInstalls(aInstallProperties) {
+ var newInstalls = [];
+ for (let installProp of aInstallProperties) {
+ let install = new MockInstall(
+ installProp.name || null,
+ installProp.type || null,
+ null
+ );
+ for (let prop in installProp) {
+ switch (prop) {
+ case "name":
+ case "type":
+ break;
+ case "sourceURI":
+ install[prop] = NetUtil.newURI(installProp[prop]);
+ break;
+ default:
+ install[prop] = installProp[prop];
+ }
+ }
+ this.addInstall(install);
+ newInstalls.push(install);
+ }
+
+ return newInstalls;
+ },
+
+ /** *** AddonProvider implementation *****/
+
+ /**
+ * Called to initialize the provider.
+ */
+ startup: function MP_startup() {
+ this.started = true;
+ },
+
+ /**
+ * Called when the provider should shutdown.
+ */
+ shutdown: function MP_shutdown() {
+ this.started = false;
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ */
+ async getAddonByID(aId) {
+ await this.queryDelayPromise;
+
+ for (let addon of this.addons) {
+ if (addon.id == aId) {
+ return addon;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ */
+ async getAddonsByTypes(aTypes) {
+ await this.queryDelayPromise;
+
+ var addons = this.addons.filter(function (aAddon) {
+ if (aTypes && !!aTypes.length && !aTypes.includes(aAddon.type)) {
+ return false;
+ }
+ return true;
+ });
+ return addons;
+ },
+
+ /**
+ * Called to get the current AddonInstalls, optionally restricting by type.
+ *
+ * @param aTypes
+ * An array of types or null to get all types
+ */
+ async getInstallsByTypes(aTypes) {
+ await this.queryDelayPromise;
+
+ var installs = this.installs.filter(function (aInstall) {
+ // Appear to have actually removed cancelled installs from the provider
+ if (aInstall.state == AddonManager.STATE_CANCELLED) {
+ return false;
+ }
+
+ if (aTypes && !!aTypes.length && !aTypes.includes(aInstall.type)) {
+ return false;
+ }
+
+ return true;
+ });
+ return installs;
+ },
+
+ /**
+ * Called when a new add-on has been enabled when only one add-on of that type
+ * can be enabled.
+ *
+ * @param aId
+ * The ID of the newly enabled add-on
+ * @param aType
+ * The type of the newly enabled add-on
+ * @param aPendingRestart
+ * true if the newly enabled add-on will only become enabled after a
+ * restart
+ */
+ addonChanged: function MP_addonChanged(aId, aType, aPendingRestart) {
+ // Not implemented
+ },
+
+ /**
+ * Update the appDisabled property for all add-ons.
+ */
+ updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() {
+ // Not needed
+ },
+
+ /**
+ * Called to get an AddonInstall to download and install an add-on from a URL.
+ *
+ * @param {string} aUrl
+ * The URL to be installed
+ * @param {object} aOptions
+ * Options for the install
+ */
+ getInstallForURL: function MP_getInstallForURL(aUrl, aOptions) {
+ // Not yet implemented
+ },
+
+ /**
+ * Called to get an AddonInstall to install an add-on from a local file.
+ *
+ * @param aFile
+ * The file to be installed
+ */
+ getInstallForFile: function MP_getInstallForFile(aFile) {
+ // Not yet implemented
+ },
+
+ /**
+ * Called to test whether installing add-ons is enabled.
+ *
+ * @return true if installing is enabled
+ */
+ isInstallEnabled: function MP_isInstallEnabled() {
+ return false;
+ },
+
+ /**
+ * Called to test whether this provider supports installing a particular
+ * mimetype.
+ *
+ * @param aMimetype
+ * The mimetype to check for
+ * @return true if the mimetype is supported
+ */
+ supportsMimetype: function MP_supportsMimetype(aMimetype) {
+ return false;
+ },
+
+ /**
+ * Called to test whether installing add-ons from a URI is allowed.
+ *
+ * @param aUri
+ * The URI being installed from
+ * @return true if installing is allowed
+ */
+ isInstallAllowed: function MP_isInstallAllowed(aUri) {
+ return false;
+ },
+};
+
+/** *** Mock Addon object for the Mock Provider *****/
+
+function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
+ // Only set required attributes.
+ this.id = aId || "";
+ this.name = aName || "";
+ this.type = aType || "extension";
+ this.version = "";
+ this.isCompatible = true;
+ this.providesUpdatesSecurely = true;
+ this.blocklistState = 0;
+ this._appDisabled = false;
+ this._userDisabled = false;
+ this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ this.scope = AddonManager.SCOPE_PROFILE;
+ this.isActive = true;
+ this.creator = "";
+ this.pendingOperations = 0;
+ this._permissions =
+ AddonManager.PERM_CAN_UNINSTALL |
+ AddonManager.PERM_CAN_ENABLE |
+ AddonManager.PERM_CAN_DISABLE |
+ AddonManager.PERM_CAN_UPGRADE |
+ AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS;
+ this.operationsRequiringRestart =
+ aOperationsRequiringRestart != undefined
+ ? aOperationsRequiringRestart
+ : AddonManager.OP_NEEDS_RESTART_INSTALL |
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL |
+ AddonManager.OP_NEEDS_RESTART_ENABLE |
+ AddonManager.OP_NEEDS_RESTART_DISABLE;
+}
+
+MockAddon.prototype = {
+ get isCorrectlySigned() {
+ if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
+ return true;
+ }
+ return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
+ },
+
+ get shouldBeActive() {
+ return (
+ !this.appDisabled &&
+ !this._userDisabled &&
+ !(this.pendingOperations & AddonManager.PENDING_UNINSTALL)
+ );
+ },
+
+ get appDisabled() {
+ return this._appDisabled;
+ },
+
+ set appDisabled(val) {
+ if (val == this._appDisabled) {
+ return;
+ }
+
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
+ "appDisabled",
+ ]);
+
+ var currentActive = this.shouldBeActive;
+ this._appDisabled = val;
+ var newActive = this.shouldBeActive;
+ this._updateActiveState(currentActive, newActive);
+ },
+
+ get userDisabled() {
+ return this._userDisabled;
+ },
+
+ set userDisabled(val) {
+ throw new Error("No. Bad.");
+ },
+
+ setUserDisabled(val) {
+ if (val == this._userDisabled) {
+ return;
+ }
+
+ var currentActive = this.shouldBeActive;
+ this._userDisabled = val;
+ var newActive = this.shouldBeActive;
+ this._updateActiveState(currentActive, newActive);
+ },
+
+ async enable() {
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ this.setUserDisabled(false);
+ },
+ async disable() {
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ this.setUserDisabled(true);
+ },
+
+ get permissions() {
+ let permissions = this._permissions;
+ if (this.appDisabled || !this._userDisabled) {
+ permissions &= ~AddonManager.PERM_CAN_ENABLE;
+ }
+ if (this.appDisabled || this._userDisabled) {
+ permissions &= ~AddonManager.PERM_CAN_DISABLE;
+ }
+ return permissions;
+ },
+
+ set permissions(val) {
+ this._permissions = val;
+ },
+
+ get applyBackgroundUpdates() {
+ return this._applyBackgroundUpdates;
+ },
+
+ set applyBackgroundUpdates(val) {
+ if (
+ val != AddonManager.AUTOUPDATE_DEFAULT &&
+ val != AddonManager.AUTOUPDATE_DISABLE &&
+ val != AddonManager.AUTOUPDATE_ENABLE
+ ) {
+ ok(false, "addon.applyBackgroundUpdates set to an invalid value: " + val);
+ }
+ this._applyBackgroundUpdates = val;
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
+ "applyBackgroundUpdates",
+ ]);
+ },
+
+ isCompatibleWith(aAppVersion, aPlatformVersion) {
+ return true;
+ },
+
+ findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+ // Tests can implement this if they need to
+ },
+
+ async getBlocklistURL() {
+ return this.blocklistURL;
+ },
+
+ uninstall(aAlwaysAllowUndo = false) {
+ if (
+ this.operationsRequiringRestart &
+ AddonManager.OP_NEED_RESTART_UNINSTALL &&
+ this.pendingOperations & AddonManager.PENDING_UNINSTALL
+ ) {
+ throw Components.Exception("Add-on is already pending uninstall");
+ }
+
+ var needsRestart =
+ aAlwaysAllowUndo ||
+ !!(
+ this.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL
+ );
+ this.pendingOperations |= AddonManager.PENDING_UNINSTALL;
+ AddonManagerPrivate.callAddonListeners(
+ "onUninstalling",
+ this,
+ needsRestart
+ );
+ if (!needsRestart) {
+ this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ this._provider.removeAddon(this);
+ } else if (
+ !(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE)
+ ) {
+ this.isActive = false;
+ }
+ },
+
+ cancelUninstall() {
+ if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL)) {
+ throw Components.Exception("Add-on is not pending uninstall");
+ }
+
+ this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ this.isActive = this.shouldBeActive;
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ },
+
+ markAsSeen() {
+ this.seen = true;
+ },
+
+ _updateActiveState(currentActive, newActive) {
+ if (currentActive == newActive) {
+ return;
+ }
+
+ if (newActive == this.isActive) {
+ this.pendingOperations -= newActive
+ ? AddonManager.PENDING_DISABLE
+ : AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ } else if (newActive) {
+ let needsRestart = !!(
+ this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE
+ );
+ this.pendingOperations |= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabling", this, needsRestart);
+ if (!needsRestart) {
+ this.isActive = newActive;
+ this.pendingOperations -= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabled", this);
+ }
+ } else {
+ let needsRestart = !!(
+ this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE
+ );
+ this.pendingOperations |= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabling", this, needsRestart);
+ if (!needsRestart) {
+ this.isActive = newActive;
+ this.pendingOperations -= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabled", this);
+ }
+ }
+ },
+};
+
+/** *** Mock AddonInstall object for the Mock Provider *****/
+
+function MockInstall(aName, aType, aAddonToInstall) {
+ this.name = aName || "";
+ // Don't expose type until download completed
+ this._type = aType || "extension";
+ this.type = null;
+ this.version = "1.0";
+ this.iconURL = "";
+ this.infoURL = "";
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.error = 0;
+ this.sourceURI = null;
+ this.file = null;
+ this.progress = 0;
+ this.maxProgress = -1;
+ this.certificate = null;
+ this.certName = "";
+ this.existingAddon = null;
+ this.addon = null;
+ this._addonToInstall = aAddonToInstall;
+ this.listeners = [];
+
+ // Another type of install listener for tests that want to check the results
+ // of code run from standard install listeners
+ this.testListeners = [];
+}
+
+MockInstall.prototype = {
+ install() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.state = AddonManager.STATE_DOWNLOADING;
+ if (!this.callListeners("onDownloadStarted")) {
+ this.state = AddonManager.STATE_CANCELLED;
+ this.callListeners("onDownloadCancelled");
+ return;
+ }
+
+ this.type = this._type;
+
+ // Adding addon to MockProvider to be implemented when needed
+ if (this._addonToInstall) {
+ this.addon = this._addonToInstall;
+ } else {
+ this.addon = new MockAddon("", this.name, this.type);
+ this.addon.version = this.version;
+ this.addon.pendingOperations = AddonManager.PENDING_INSTALL;
+ }
+ this.addon.install = this;
+ if (this.existingAddon) {
+ if (!this.addon.id) {
+ this.addon.id = this.existingAddon.id;
+ }
+ this.existingAddon.pendingUpgrade = this.addon;
+ this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE;
+ }
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.callListeners("onDownloadEnded");
+ // fall through
+ case AddonManager.STATE_DOWNLOADED:
+ this.state = AddonManager.STATE_INSTALLING;
+ if (!this.callListeners("onInstallStarted")) {
+ this.state = AddonManager.STATE_CANCELLED;
+ this.callListeners("onInstallCancelled");
+ return;
+ }
+
+ let needsRestart =
+ this.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL;
+ AddonManagerPrivate.callAddonListeners(
+ "onInstalling",
+ this.addon,
+ needsRestart
+ );
+ if (!needsRestart) {
+ AddonManagerPrivate.callAddonListeners("onInstalled", this.addon);
+ }
+
+ this.state = AddonManager.STATE_INSTALLED;
+ this.callListeners("onInstallEnded");
+ break;
+ case AddonManager.STATE_DOWNLOADING:
+ case AddonManager.STATE_CHECKING_UPDATE:
+ case AddonManager.STATE_INSTALLING:
+ // Installation is already running
+ return;
+ default:
+ ok(false, "Cannot start installing when state = " + this.state);
+ }
+ },
+
+ cancel() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.state = AddonManager.STATE_CANCELLED;
+ break;
+ case AddonManager.STATE_INSTALLED:
+ this.state = AddonManager.STATE_CANCELLED;
+ this._provider.removeInstall(this);
+ this.callListeners("onInstallCancelled");
+ break;
+ default:
+ // Handling cancelling when downloading to be implemented when needed
+ ok(false, "Cannot cancel when state = " + this.state);
+ }
+ },
+
+ addListener(aListener) {
+ if (!this.listeners.some(i => i == aListener)) {
+ this.listeners.push(aListener);
+ }
+ },
+
+ removeListener(aListener) {
+ this.listeners = this.listeners.filter(i => i != aListener);
+ },
+
+ addTestListener(aListener) {
+ if (!this.testListeners.some(i => i == aListener)) {
+ this.testListeners.push(aListener);
+ }
+ },
+
+ removeTestListener(aListener) {
+ this.testListeners = this.testListeners.filter(i => i != aListener);
+ },
+
+ callListeners(aMethod) {
+ var result = AddonManagerPrivate.callInstallListeners(
+ aMethod,
+ this.listeners,
+ this,
+ this.addon
+ );
+
+ // Call test listeners after standard listeners to remove race condition
+ // between standard and test listeners
+ for (let listener of this.testListeners) {
+ try {
+ if (aMethod in listener) {
+ if (listener[aMethod](this, this.addon) === false) {
+ result = false;
+ }
+ }
+ } catch (e) {
+ ok(false, "Test listener threw exception: " + e);
+ }
+ }
+
+ return result;
+ },
+};
+
+function waitForCondition(condition, nextTest, errorMsg) {
+ let tries = 0;
+ let interval = setInterval(function () {
+ if (tries >= 30) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ let moveOn = function () {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+// Wait for and then acknowledge (by pressing the primary button) the
+// given notification.
+function promiseNotification(id = "addon-webext-permissions") {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = PopupNotifications.getNotification(id);
+ if (notification) {
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ PopupNotifications.panel.firstElementChild.button.click();
+ resolve();
+ }
+ }
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name = "addon-webext-permissions") {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ resolve(PopupNotifications.panel.firstChild);
+ }
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function waitAppMenuNotificationShown(
+ id,
+ addonId,
+ accept = false,
+ win = window
+) {
+ const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+ );
+ return new Promise(resolve => {
+ let { document, PanelUI } = win;
+
+ async function popupshown() {
+ let notification = AppMenuNotifications.activeNotification;
+ if (!notification) {
+ return;
+ }
+
+ is(notification.id, id, `${id} notification shown`);
+ ok(PanelUI.isNotificationPanelOpen, "notification panel open");
+
+ PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);
+
+ if (id == "addon-installed" && addonId) {
+ let addon = await AddonManager.getAddonByID(addonId);
+ if (!addon) {
+ ok(false, `Addon with id "${addonId}" not found`);
+ }
+ let hidden = !(
+ addon.permissions &
+ AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
+ );
+ let checkbox = document.getElementById("addon-incognito-checkbox");
+ is(checkbox.hidden, hidden, "checkbox visibility is correct");
+ }
+ if (accept) {
+ let popupnotificationID = PanelUI._getPopupId(notification);
+ let popupnotification = document.getElementById(popupnotificationID);
+ popupnotification.button.click();
+ }
+
+ resolve();
+ }
+ // If it's already open just run the test.
+ let notification = AppMenuNotifications.activeNotification;
+ if (notification && PanelUI.isNotificationPanelOpen) {
+ popupshown();
+ return;
+ }
+ PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function acceptAppMenuNotificationWhenShown(id, addonId) {
+ return waitAppMenuNotificationShown(id, addonId, true);
+}
+
+/* HTML view helpers */
+async function loadInitialView(type, opts) {
+ if (type) {
+ // Force the first page load to be the view we want.
+ let viewId;
+ if (type.startsWith("addons://")) {
+ viewId = type;
+ } else {
+ viewId =
+ type == "discover" ? "addons://discover/" : `addons://list/${type}`;
+ }
+ Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, viewId);
+ }
+
+ let loadCallback;
+ let loadCallbackDone = Promise.resolve();
+
+ if (opts && opts.loadCallback) {
+ loadCallback = win => {
+ loadCallbackDone = (async () => {
+ // Wait for the test code to finish running before proceeding.
+ await opts.loadCallback(win);
+ })();
+ };
+ }
+
+ let win = await open_manager(null, null, loadCallback);
+ if (!opts || !opts.withAnimations) {
+ win.document.body.setAttribute("skip-animations", "");
+ }
+
+ // Let any load callback code to run before the rest of the test continues.
+ await loadCallbackDone;
+
+ return win;
+}
+
+function getSection(doc, className) {
+ return doc.querySelector(`section.${className}`);
+}
+
+function waitForViewLoad(win) {
+ return wait_for_view_load(win, undefined, true);
+}
+
+function closeView(win) {
+ return close_manager(win);
+}
+
+function switchView(win, type) {
+ return new CategoryUtilities(win).openType(type);
+}
+
+function isCategoryVisible(win, type) {
+ return new CategoryUtilities(win).isTypeVisible(type);
+}
+
+function mockPromptService() {
+ let { prompt } = Services;
+ let promptService = {
+ // The prompt returns 1 for cancelled and 0 for accepted.
+ _response: 1,
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx: () => promptService._response,
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+ return promptService;
+}
+
+function assertHasPendingUninstalls(addonList, expectedPendingUninstallsCount) {
+ const pendingUninstalls = addonList.querySelector(
+ "message-bar-stack.pending-uninstall"
+ );
+ ok(pendingUninstalls, "Got a pending-uninstall message-bar-stack");
+ is(
+ pendingUninstalls.childElementCount,
+ expectedPendingUninstallsCount,
+ "Got a message bar in the pending-uninstall message-bar-stack"
+ );
+}
+
+function assertHasPendingUninstallAddon(addonList, addon) {
+ const pendingUninstalls = addonList.querySelector(
+ "message-bar-stack.pending-uninstall"
+ );
+ const addonPendingUninstall = addonList.getPendingUninstallBar(addon);
+ ok(
+ addonPendingUninstall,
+ "Got expected message-bar for the pending uninstall test extension"
+ );
+ is(
+ addonPendingUninstall.parentNode,
+ pendingUninstalls,
+ "pending uninstall bar should be part of the message-bar-stack"
+ );
+ is(
+ addonPendingUninstall.getAttribute("addon-id"),
+ addon.id,
+ "Got expected addon-id attribute on the pending uninstall message-bar"
+ );
+}
+
+async function testUndoPendingUninstall(addonList, addon) {
+ const addonPendingUninstall = addonList.getPendingUninstallBar(addon);
+ const undoButton = addonPendingUninstall.querySelector("button[action=undo]");
+ ok(undoButton, "Got undo action button in the pending uninstall message-bar");
+
+ info(
+ "Clicking the pending uninstall undo button and wait for addon card rendered"
+ );
+ const updated = BrowserTestUtils.waitForEvent(addonList, "add");
+ undoButton.click();
+ await updated;
+
+ ok(
+ addon && !(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "The addon pending uninstall cancelled"
+ );
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+function cleanupPendingNotifications() {
+ const { ExtensionsUI } = ChromeUtils.importESModule(
+ "resource:///modules/ExtensionsUI.sys.mjs"
+ );
+ info("Cleanup any pending notification before exiting the test");
+ const keys = ChromeUtils.nondeterministicGetWeakSetKeys(
+ ExtensionsUI.pendingNotifications
+ );
+ if (keys) {
+ keys.forEach(key => ExtensionsUI.pendingNotifications.delete(key));
+ }
+}
+
+function promisePermissionPrompt(addonId) {
+ return BrowserUtils.promiseObserved(
+ "webextension-permission-prompt",
+ subject => {
+ const { info } = subject.wrappedJSObject || {};
+ return !addonId || (info.addon && info.addon.id === addonId);
+ }
+ ).then(({ subject }) => {
+ return subject.wrappedJSObject.info;
+ });
+}
+
+async function handlePermissionPrompt({
+ addonId,
+ reject = false,
+ assertIcon = true,
+} = {}) {
+ const info = await promisePermissionPrompt(addonId);
+ // Assert that info.addon and info.icon are defined as expected.
+ is(
+ info.addon && info.addon.id,
+ addonId,
+ "Got the AddonWrapper in the permission prompt info"
+ );
+
+ if (assertIcon) {
+ Assert.notEqual(
+ info.icon,
+ null,
+ "Got an addon icon in the permission prompt info"
+ );
+ }
+
+ if (reject) {
+ info.reject();
+ } else {
+ info.resolve();
+ }
+}
+
+async function switchToDetailView({ id, win }) {
+ let card = getAddonCard(win, id);
+ ok(card, `Addon card found for ${id}`);
+ ok(!card.querySelector("addon-details"), "The card doesn't have details");
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(
+ card.querySelector(".addon-name-link"),
+ { clickCount: 1 },
+ win
+ );
+ await loaded;
+ card = getAddonCard(win, id);
+ ok(card.querySelector("addon-details"), "The card does have details");
+ return card;
+}
diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
new file mode 100644
index 0000000000..f3a683e8d5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
@@ -0,0 +1,615 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+/* exported installTestExtension, addCommonAbuseReportTestTasks,
+ * createPromptConfirmEx, DEFAULT_BUILTIN_THEME_ID,
+ * gManagerWindow, handleSubmitRequest, makeWidgetId,
+ * waitForNewWindow, waitClosedWindow, AbuseReporter,
+ * AbuseReporterTestUtils, AddonTestUtils
+ */
+
+/* global MockProvider, loadInitialView, closeView */
+
+const { AbuseReporter } = ChromeUtils.importESModule(
+ "resource://gre/modules/AbuseReporter.sys.mjs"
+);
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+);
+
+const { makeWidgetId } = ExtensionCommon;
+
+const ADDON_ID = "test-extension-to-report@mochi.test";
+const REPORT_ENTRY_POINT = "menu";
+const BASE_TEST_MANIFEST = {
+ name: "Fake extension to report",
+ author: "Fake author",
+ homepage_url: "https://fake.extension.url/",
+};
+const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org";
+const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test";
+const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test";
+const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test";
+const EXT_SYSTEM_ADDON_ID = "test-system-addon@mochi.test";
+const EXT_UNSUPPORTED_TYPE_ADDON_ID = "report-unsupported-type@mochi.test";
+const THEME_NO_UNINSTALL_ID = "theme-without-perm-can-uninstall@mochi.test";
+
+let gManagerWindow;
+
+AddonTestUtils.initMochitest(this);
+
+async function openAboutAddons(type = "extension") {
+ gManagerWindow = await loadInitialView(type);
+}
+
+async function closeAboutAddons() {
+ if (gManagerWindow) {
+ await closeView(gManagerWindow);
+ gManagerWindow = null;
+ }
+}
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ let listener = win => {
+ Services.obs.removeObserver(listener, "toplevel-window-ready");
+ resolve(win);
+ };
+
+ Services.obs.addObserver(listener, "toplevel-window-ready");
+ });
+}
+
+function waitClosedWindow(win) {
+ return new Promise((resolve, reject) => {
+ function onWindowClosed() {
+ if (win && !win.closed) {
+ // If a specific window reference has been passed, then check
+ // that the window is closed before resolving the promise.
+ return;
+ }
+ Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+ resolve();
+ }
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
+ });
+}
+
+async function installTestExtension(
+ id = ADDON_ID,
+ type = "extension",
+ manifest = {}
+) {
+ let additionalProps = {
+ icons: {
+ 32: "test-icon.png",
+ },
+ };
+
+ switch (type) {
+ case "theme":
+ additionalProps = {
+ ...additionalProps,
+ theme: {
+ colors: {
+ frame: "#a14040",
+ tab_background_text: "#fac96e",
+ },
+ },
+ };
+ break;
+
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
+ // implementation is also removed.
+ case "sitepermission-deprecated":
+ additionalProps = {
+ name: "WebMIDI test addon for https://mochi.test",
+ install_origins: ["https://mochi.test"],
+ site_permissions: ["midi"],
+ };
+ break;
+ case "extension":
+ break;
+ default:
+ throw new Error(`Unexpected addon type: ${type}`);
+ }
+
+ const extensionOpts = {
+ manifest: {
+ ...BASE_TEST_MANIFEST,
+ ...additionalProps,
+ ...manifest,
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ };
+
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
+ // implementation is also removed.
+ if (type === "sitepermission-deprecated") {
+ const xpi = AddonTestUtils.createTempWebExtensionFile(extensionOpts);
+ const addon = await AddonManager.installTemporaryAddon(xpi);
+ // The extension object that ExtensionTestUtils.loadExtension returns for
+ // mochitest is pretty tight to the Extension class, and so for now this
+ // returns a more minimal `extension` test object which only provides the
+ // `unload` method.
+ //
+ // For the purpose of the abuse reports tests that are using this helper
+ // this should be already enough.
+ return {
+ addon,
+ unload: () => addon.uninstall(),
+ };
+ }
+
+ const extension = ExtensionTestUtils.loadExtension(extensionOpts);
+ await extension.startup();
+ return extension;
+}
+
+function handleSubmitRequest({ request, response }) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.write("{}");
+}
+
+const AbuseReportTestUtils = {
+ _mockProvider: null,
+ _mockServer: null,
+ _abuseRequestHandlers: [],
+
+ // Mock addon details API endpoint.
+ amoAddonDetailsMap: new Map(),
+
+ // Setup the test environment by setting the expected prefs and
+ // initializing MockProvider and the mock AMO server.
+ async setup() {
+ // Enable html about:addons and the abuse reporting and
+ // set the api endpoints url to the mock service.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.abuseReport.enabled", true],
+ ["extensions.abuseReport.url", "http://test.addons.org/api/report/"],
+ [
+ "extensions.abuseReport.amoDetailsURL",
+ "http://test.addons.org/api/addons/addon/",
+ ],
+ ],
+ });
+
+ this._setupMockProvider();
+ this._setupMockServer();
+ },
+
+ // Returns the currently open abuse report dialog window (if any).
+ getReportDialog() {
+ return Services.ww.getWindowByName("addons-abuse-report-dialog");
+ },
+
+ // Returns the parameters related to the report dialog (if any).
+ getReportDialogParams() {
+ const win = this.getReportDialog();
+ return win && win.arguments[0] && win.arguments[0].wrappedJSObject;
+ },
+
+ // Returns a reference to the addon-abuse-report element from the currently
+ // open abuse report.
+ getReportPanel() {
+ const win = this.getReportDialog();
+ ok(win, "Got an abuse report dialog open");
+ return win && win.document.querySelector("addon-abuse-report");
+ },
+
+ // Returns the list of abuse report reasons.
+ getReasons(abuseReportEl) {
+ return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS);
+ },
+
+ // Returns the info related to a given abuse report reason.
+ getReasonInfo(abuseReportEl, reason) {
+ return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason];
+ },
+
+ async promiseReportOpened({ addonId, reportEntryPoint, managerWindow }) {
+ let abuseReportEl;
+
+ if (!this.getReportDialog()) {
+ info("Wait for the report dialog window");
+ const dialog = await waitForNewWindow();
+ is(dialog, this.getReportDialog(), "Report dialog opened");
+ }
+
+ info("Wait for the abuse report panel render");
+ abuseReportEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ ok(abuseReportEl, "Got an abuse report panel");
+ is(
+ abuseReportEl.addon && abuseReportEl.addon.id,
+ addonId,
+ "Abuse Report panel rendered for the expected addonId"
+ );
+ is(
+ abuseReportEl._report && abuseReportEl._report.reportEntryPoint,
+ reportEntryPoint,
+ "Abuse Report panel rendered for the expected reportEntryPoint"
+ );
+
+ return abuseReportEl;
+ },
+
+ // Return a promise resolved when the currently open report panel
+ // is closed.
+ // Also asserts that a specific report panel element has been closed,
+ // if one has been provided through the optional panel parameter.
+ async promiseReportClosed(panel) {
+ const win = panel ? panel.ownerGlobal : this.getReportDialog();
+ if (!win || win.closed) {
+ throw Error("Expected report dialog not found or already closed");
+ }
+
+ await waitClosedWindow(win);
+ // Assert that the panel has been closed (if the caller has passed it).
+ if (panel) {
+ ok(!panel.ownerGlobal, "abuse report dialog closed");
+ }
+ },
+
+ // Returns a promise resolved when the report panel has been rendered
+ // (rejects is there is no dialog currently open).
+ async promiseReportDialogRendered() {
+ const params = this.getReportDialogParams();
+ if (!params) {
+ throw new Error("abuse report dialog not found");
+ }
+ return params.promiseReportPanel;
+ },
+
+ // Given a `requestHandler` function, an HTTP server handler function
+ // to use to handle a report submit request received by the mock AMO server),
+ // returns a promise resolved when the mock AMO server has received and
+ // handled the report submit request.
+ async promiseReportSubmitHandled(requestHandler) {
+ if (typeof requestHandler !== "function") {
+ throw new Error("requestHandler should be a function");
+ }
+ return new Promise((resolve, reject) => {
+ this._abuseRequestHandlers.unshift({ resolve, reject, requestHandler });
+ });
+ },
+
+ // Return a promise resolved to the abuse report panel element,
+ // once its rendering is completed.
+ // If abuseReportEl is undefined, it looks for the currently opened
+ // report panel.
+ async promiseReportRendered(abuseReportEl) {
+ let el = abuseReportEl;
+
+ if (!el) {
+ const win = this.getReportDialog();
+ if (!win) {
+ await waitForNewWindow();
+ }
+
+ el = await this.promiseReportDialogRendered();
+ ok(el, "Got an abuse report panel");
+ }
+
+ return el._radioCheckedReason
+ ? el
+ : BrowserTestUtils.waitForEvent(
+ el,
+ "abuse-report:updated",
+ "Wait the abuse report panel to be rendered"
+ ).then(() => el);
+ },
+
+ // A promise resolved when the given abuse report panel element
+ // has been rendered. If a panel name ("reasons" or "submit") is
+ // passed as a second parameter, it also asserts that the panel is
+ // updated to the expected view mode.
+ async promiseReportUpdated(abuseReportEl, panel) {
+ const evt = await BrowserTestUtils.waitForEvent(
+ abuseReportEl,
+ "abuse-report:updated",
+ "Wait abuse report panel update"
+ );
+
+ if (panel) {
+ is(evt.detail.panel, panel, `Got a "${panel}" update event`);
+
+ const el = abuseReportEl;
+ switch (evt.detail.panel) {
+ case "reasons":
+ ok(!el._reasonsPanel.hidden, "Reasons panel should be visible");
+ ok(el._submitPanel.hidden, "Submit panel should be hidden");
+ break;
+ case "submit":
+ ok(el._reasonsPanel.hidden, "Reasons panel should be hidden");
+ ok(!el._submitPanel.hidden, "Submit panel should be visible");
+ break;
+ }
+ }
+ },
+
+ // Returns a promise resolved once the expected number of abuse report
+ // message bars have been created.
+ promiseMessageBars(expectedMessageBarCount) {
+ return new Promise(resolve => {
+ const details = [];
+ function listener(evt) {
+ details.push(evt.detail);
+ if (details.length >= expectedMessageBarCount) {
+ cleanup();
+ resolve(details);
+ }
+ }
+ function cleanup() {
+ if (gManagerWindow) {
+ gManagerWindow.document.removeEventListener(
+ "abuse-report:new-message-bar",
+ listener
+ );
+ }
+ }
+ gManagerWindow.document.addEventListener(
+ "abuse-report:new-message-bar",
+ listener
+ );
+ });
+ },
+
+ async assertFluentStrings(containerEl) {
+ // Make sure all localized elements have defined Fluent strings.
+ let localizedEls = Array.from(
+ containerEl.querySelectorAll("[data-l10n-id]")
+ );
+ if (containerEl.getAttribute("data-l10n-id")) {
+ localizedEls.push(containerEl);
+ }
+ ok(localizedEls.length, "Got localized elements");
+ for (let el of localizedEls) {
+ const l10nId = el.getAttribute("data-l10n-id");
+ const l10nAttrs = el.getAttribute("data-l10n-attrs");
+ if (!l10nAttrs) {
+ await TestUtils.waitForCondition(
+ () => el.textContent !== "",
+ `Element with Fluent id '${l10nId}' should not be empty`
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () => el.message !== "",
+ `Message attribute of the element with Fluent id '${l10nId}'
+ should not be empty`
+ );
+ }
+ }
+ },
+
+ // Assert that the report action visibility on the addon card
+ // for the given about:addons windows and extension id.
+ async assertReportActionVisibility(gManagerWindow, extId, expectShown) {
+ let addonCard = gManagerWindow.document.querySelector(
+ `addon-list addon-card[addon-id="${extId}"]`
+ );
+ ok(addonCard, `Got the addon-card for the ${extId} test extension`);
+
+ let reportButton = addonCard.querySelector("[action=report]");
+ ok(reportButton, `Got the report action for ${extId}`);
+ Assert.equal(
+ reportButton.hidden,
+ !expectShown,
+ `${extId} report action should be ${expectShown ? "shown" : "hidden"}`
+ );
+ },
+
+ // Assert that the report action is hidden on the addon card
+ // for the given about:addons windows and extension id.
+ assertReportActionHidden(gManagerWindow, extId) {
+ return this.assertReportActionVisibility(gManagerWindow, extId, false);
+ },
+
+ // Assert that the report action is shown on the addon card
+ // for the given about:addons windows and extension id.
+ assertReportActionShown(gManagerWindow, extId) {
+ return this.assertReportActionVisibility(gManagerWindow, extId, true);
+ },
+
+ // Assert that the report panel is hidden (or closed if the report
+ // panel is opened in its own dialog window).
+ async assertReportPanelHidden() {
+ const win = this.getReportDialog();
+ ok(!win, "Abuse Report dialog should be initially hidden");
+ },
+
+ createMockAddons(mockProviderAddons) {
+ this._mockProvider.createAddons(mockProviderAddons);
+ },
+
+ async clickPanelButton(buttonEl, { label = undefined } = {}) {
+ info(`Clicking the '${buttonEl.textContent.trim() || label}' button`);
+ // NOTE: ideally this should synthesize the mouse event,
+ // we call the click method to prevent intermittent timeouts
+ // due to the mouse event not received by the target element.
+ buttonEl.click();
+ },
+
+ triggerNewReport(addonId, reportEntryPoint) {
+ gManagerWindow.openAbuseReport({ addonId, reportEntryPoint });
+ },
+
+ triggerSubmit(reason, message) {
+ const reportEl =
+ this.getReportDialog().document.querySelector("addon-abuse-report");
+ reportEl._form.elements.message.value = message;
+ reportEl._form.elements.reason.value = reason;
+ reportEl.submit();
+ },
+
+ async openReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) {
+ // Close the current about:addons window if it has been leaved open from
+ // a previous test case failure.
+ if (gManagerWindow) {
+ await closeAboutAddons();
+ }
+
+ await openAboutAddons();
+
+ let promiseReportPanel = waitForNewWindow().then(() =>
+ this.promiseReportDialogRendered()
+ );
+
+ this.triggerNewReport(addonId, reportEntryPoint);
+
+ const panelEl = await promiseReportPanel;
+ await this.promiseReportRendered(panelEl);
+ is(panelEl.addonId, addonId, `Got Abuse Report panel for ${addonId}`);
+
+ return panelEl;
+ },
+
+ async closeReportPanel(panelEl) {
+ const onceReportClosed = AbuseReportTestUtils.promiseReportClosed(panelEl);
+
+ info("Cancel report and wait the dialog to be closed");
+ panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel"));
+
+ await onceReportClosed;
+ },
+
+ // Internal helper methods.
+
+ _setupMockProvider() {
+ this._mockProvider = new MockProvider();
+ this._mockProvider.createAddons([
+ {
+ id: THEME_NO_UNINSTALL_ID,
+ name: "This theme cannot be uninstalled",
+ version: "1.1",
+ creator: { name: "Theme creator", url: "http://example.com/creator" },
+ type: "theme",
+ permissions: 0,
+ },
+ {
+ id: EXT_WITH_PRIVILEGED_URL_ID,
+ name: "This extension has an unexpected privileged creator URL",
+ version: "1.1",
+ creator: { name: "creator", url: "about:config" },
+ type: "extension",
+ },
+ {
+ id: EXT_SYSTEM_ADDON_ID,
+ name: "This is a system addon",
+ version: "1.1",
+ creator: { name: "creator", url: "http://example.com/creator" },
+ type: "extension",
+ isSystem: true,
+ },
+ {
+ id: EXT_UNSUPPORTED_TYPE_ADDON_ID,
+ name: "This is a fake unsupported addon type",
+ version: "1.1",
+ type: "unsupported_addon_type",
+ },
+ {
+ id: EXT_LANGPACK_ADDON_ID,
+ name: "This is a fake langpack",
+ version: "1.1",
+ type: "locale",
+ },
+ {
+ id: EXT_DICTIONARY_ADDON_ID,
+ name: "This is a fake dictionary",
+ version: "1.1",
+ type: "dictionary",
+ },
+ ]);
+ },
+
+ _setupMockServer() {
+ if (this._mockServer) {
+ return;
+ }
+
+ // Init test report api server.
+ const server = AddonTestUtils.createHttpServer({
+ hosts: ["test.addons.org"],
+ });
+ this._mockServer = server;
+
+ server.registerPathHandler("/api/report/", (request, response) => {
+ const stream = request.bodyInputStream;
+ const buffer = NetUtil.readInputStream(stream, stream.available());
+ const data = new TextDecoder().decode(buffer);
+ const promisedHandler = this._abuseRequestHandlers.pop();
+ if (promisedHandler) {
+ const { requestHandler, resolve, reject } = promisedHandler;
+ try {
+ requestHandler({ data, request, response });
+ resolve();
+ } catch (err) {
+ ok(false, `Unexpected requestHandler error ${err} ${err.stack}\n`);
+ reject(err);
+ }
+ } else {
+ ok(false, `Unexpected request: ${request.path} ${data}`);
+ }
+ });
+
+ server.registerPrefixHandler("/api/addons/addon/", (request, response) => {
+ const addonId = request.path.split("/").pop();
+ if (!this.amoAddonDetailsMap.has(addonId)) {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.write(JSON.stringify({ detail: "Not found." }));
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "Success");
+ response.write(JSON.stringify(this.amoAddonDetailsMap.get(addonId)));
+ }
+ });
+ server.registerPathHandler(
+ "/assets/fake-icon-url.png",
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "Success");
+ response.write("");
+ response.finish();
+ }
+ );
+ },
+};
+
+function createPromptConfirmEx({
+ remove = false,
+ report = false,
+ expectCheckboxHidden = false,
+} = {}) {
+ return (...args) => {
+ const checkboxState = args.pop();
+ const checkboxMessage = args.pop();
+ is(
+ checkboxState && checkboxState.value,
+ false,
+ "checkboxState should be initially false"
+ );
+ if (expectCheckboxHidden) {
+ ok(
+ !checkboxMessage,
+ "Should not have a checkboxMessage in promptService.confirmEx call"
+ );
+ } else {
+ ok(
+ checkboxMessage,
+ "Got a checkboxMessage in promptService.confirmEx call"
+ );
+ }
+
+ // Report checkbox selected.
+ checkboxState.value = report;
+
+ // Remove accepted.
+ return remove ? 0 : 1;
+ };
+}
diff --git a/toolkit/mozapps/extensions/test/browser/head_disco.js b/toolkit/mozapps/extensions/test/browser/head_disco.js
new file mode 100644
index 0000000000..64c346f3dd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head_disco.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* eslint max-len: ["error", 80] */
+
+/* exported DISCOAPI_DEFAULT_FIXTURE, getCardContainer,
+ getDiscoveryElement, promiseAddonInstall, promiseDiscopaneUpdate,
+ promiseEvent, promiseObserved, readAPIResponseFixture */
+
+/* globals RELATIVE_DIR, promisePopupNotificationShown,
+ waitAppMenuNotificationShown */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const {
+ ExtensionUtils: { promiseEvent, promiseObserved },
+} = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs");
+
+AddonTestUtils.initMochitest(this);
+
+// The response to the discovery API, as documented at:
+// https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
+//
+// The tests using this fixure are meant to verify that the discopane works
+// with the latest AMO API.
+// The following fixure file should be kept in sync with the content of
+// latest AMO API response, e.g. from
+//
+// https://addons.allizom.org/api/v4/discovery/?lang=en-US
+//
+// The response must contain at least one theme, and one extension.
+const DISCOAPI_DEFAULT_FIXTURE = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ ...RELATIVE_DIR.split("/"),
+ "discovery",
+ "api_response.json"
+);
+
+// Read the content of API_RESPONSE_FILE, and replaces any embedded URLs with
+// URLs that point to the `amoServer` test server.
+async function readAPIResponseFixture(
+ amoTestHost,
+ fixtureFilePath = DISCOAPI_DEFAULT_FIXTURE
+) {
+ let apiText = await IOUtils.readUTF8(fixtureFilePath);
+ apiText = apiText.replace(/\bhttps?:\/\/[^"]+(?=")/g, url => {
+ try {
+ url = new URL(url);
+ } catch (e) {
+ // Responses may contain "http://*/*"; ignore it.
+ return url;
+ }
+ // In this test, we only need to distinguish between different file types,
+ // so just use the file extension as path name for amoServer.
+ let ext = url.pathname.split(".").pop();
+ return `http://${amoTestHost}/${ext}?${url.pathname}${url.search}`;
+ });
+
+ return apiText;
+}
+
+// Wait until the current `<discovery-pane>` element has finished loading its
+// cards. This can be used after the cards have been loaded.
+function promiseDiscopaneUpdate(win) {
+ let { cardsReady } = getCardContainer(win);
+ ok(cardsReady, "Discovery cards should have started to initialize");
+ return cardsReady;
+}
+
+function getCardContainer(win) {
+ return getDiscoveryElement(win).querySelector("recommended-addon-list");
+}
+
+function getDiscoveryElement(win) {
+ return win.document.querySelector("discovery-pane");
+}
+
+// A helper that waits until an installation has been requested from `amoServer`
+// and proceeds with approving the installation.
+async function promiseAddonInstall(
+ amoServer,
+ extensionData,
+ expectedTelemetryInfo = { source: "disco", taarRecommended: false }
+) {
+ let description = extensionData.manifest.description;
+ let xpiFile = AddonTestUtils.createTempWebExtensionFile(extensionData);
+ amoServer.registerFile("/xpi", xpiFile);
+
+ let addonId =
+ extensionData.manifest?.browser_specific_settings?.gecko?.id ||
+ extensionData.manifest?.applications?.gecko?.id;
+ let installedPromise = waitAppMenuNotificationShown(
+ "addon-installed",
+ addonId,
+ true
+ );
+
+ if (!extensionData.manifest.theme) {
+ info(`${description}: Waiting for permission prompt`);
+ // Extensions have install prompts.
+ let panel = await promisePopupNotificationShown("addon-webext-permissions");
+ panel.button.click();
+ } else {
+ info(`${description}: Waiting for install prompt`);
+ let panel = await promisePopupNotificationShown(
+ "addon-install-confirmation"
+ );
+ panel.button.click();
+ }
+
+ info("Waiting for post-install doorhanger");
+ await installedPromise;
+
+ let addon = await AddonManager.getAddonByID(addonId);
+ Assert.deepEqual(
+ addon.installTelemetryInfo,
+ expectedTelemetryInfo,
+ "The installed add-on should have the expected telemetry info"
+ );
+}
diff --git a/toolkit/mozapps/extensions/test/browser/moz.build b/toolkit/mozapps/extensions/test/browser/moz.build
new file mode 100644
index 0000000000..4cc6314d0e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser.toml",
+]
+
+addons = [
+ "browser_dragdrop1",
+ "browser_dragdrop2",
+ "browser_dragdrop_incompat",
+ "browser_installssl",
+ "browser_theme",
+ "options_signed",
+]
+
+output_dir = (
+ OBJDIR_FILES._tests.testing.mochitest.browser.toolkit.mozapps.extensions.test.browser.addons
+)
+
+for addon in addons:
+ for file_type in ["xpi", "zip"]:
+ indir = "addons/%s" % addon
+ path = "%s.%s" % (indir, file_type)
+
+ GeneratedFile(path, script="../create_xpi.py", inputs=[indir])
+
+ output_dir += ["!%s" % path]
diff --git a/toolkit/mozapps/extensions/test/browser/redirect.sjs b/toolkit/mozapps/extensions/test/browser/redirect.sjs
new file mode 100644
index 0000000000..8f9d1c08af
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/redirect.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response) {
+ dump("*** Received redirect for " + request.queryString + "\n");
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", request.queryString, false);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/sandboxed.html b/toolkit/mozapps/extensions/test/browser/sandboxed.html
new file mode 100644
index 0000000000..219426f0a9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/sandboxed.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ Sandboxed page
+ </body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^ b/toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^
new file mode 100644
index 0000000000..4705ce9ded
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts;
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
new file mode 100644
index 0000000000..383d2a0986
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+let events = [];
+let resultEl = document.getElementById("result");
+[ "onEnabling",
+ "onEnabled",
+ "onDisabling",
+ "onDisabled",
+ "onInstalling",
+ "onInstalled",
+ "onUninstalling",
+ "onUninstalled",
+ "onOperationCancelled",
+].forEach(event => {
+ navigator.mozAddonManager.addEventListener(event, data => {
+ let obj = {event, id: data.id, needsRestart: data.needsRestart};
+ events.push(JSON.stringify(obj));
+ resultEl.textContent = events.join("\n");
+ });
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
new file mode 100644
index 0000000000..141f09cc61
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+document.getElementById("result").textContent = ("mozAddonManager" in window.navigator);
+</script>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml
new file mode 100644
index 0000000000..6e3ba328ec
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <browser id="frame" disablehistory="true" flex="1" type="content"
+ src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html"/>
+</window>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
new file mode 100644
index 0000000000..1467699789
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<iframe id="frame" height="200" width="200" src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html">
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
new file mode 100644
index 0000000000..e1f96a0b0c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<script type="text/javascript">
+/* exported openWindow, navigate, check */
+var nav, win;
+
+function openWindow() {
+ return new Promise(resolve => {
+ win = window.open(window.location);
+
+ win.addEventListener("load", function listener() {
+ nav = win.navigator;
+ resolve();
+ });
+ });
+}
+
+function navigate() {
+ win.location = "http://example.com/";
+}
+
+function check() {
+ return "mozAddonManager" in nav;
+}
+</script>
+</body>
+</html>