summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/cloudfile
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mail/components/cloudfile/cloudFileAccounts.jsm215
-rw-r--r--comm/mail/components/cloudfile/content/selectDialog.js17
-rw-r--r--comm/mail/components/cloudfile/content/selectDialog.xhtml32
-rw-r--r--comm/mail/components/cloudfile/jar.mn7
-rw-r--r--comm/mail/components/cloudfile/moz.build14
-rw-r--r--comm/mail/components/cloudfile/test/browser/browser.ini13
-rw-r--r--comm/mail/components/cloudfile/test/browser/browser_repeat_upload.js246
-rw-r--r--comm/mail/components/cloudfile/test/browser/files/green_eggs.txt1
-rw-r--r--comm/mail/components/cloudfile/test/browser/files/icon.svg7
-rw-r--r--comm/mail/components/cloudfile/test/browser/files/management.html10
-rw-r--r--comm/mail/components/cloudfile/test/browser/head.js48
11 files changed, 610 insertions, 0 deletions
diff --git a/comm/mail/components/cloudfile/cloudFileAccounts.jsm b/comm/mail/components/cloudfile/cloudFileAccounts.jsm
new file mode 100644
index 0000000000..3cb478f60f
--- /dev/null
+++ b/comm/mail/components/cloudfile/cloudFileAccounts.jsm
@@ -0,0 +1,215 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["cloudFileAccounts"];
+
+var ACCOUNT_ROOT = "mail.cloud_files.accounts.";
+
+var { EventEmitter } = ChromeUtils.importESModule(
+ "resource://gre/modules/EventEmitter.sys.mjs"
+);
+
+var cloudFileAccounts = new (class extends EventEmitter {
+ get constants() {
+ return {
+ offlineErr: 0x80550014, // NS_MSG_ERROR_OFFLINE
+ authErr: 0x8055001e, // NS_MSG_USER_NOT_AUTHENTICATED
+ uploadErr: 0x8055311a, // NS_MSG_ERROR_ATTACHING_FILE
+ uploadWouldExceedQuota: 0x8055311b,
+ uploadExceedsFileLimit: 0x8055311c,
+ uploadCancelled: 0x8055311d,
+ uploadErrWithCustomMessage: 0x8055311f,
+ renameErr: 0x80553120,
+ renameErrWithCustomMessage: 0x80553121,
+ renameNotSupported: 0x80553122,
+ deleteErr: 0x80553123,
+ attachmentErr: 0x80553124,
+ accountErr: 0x80553125,
+ };
+ }
+
+ constructor() {
+ super();
+ this._providers = new Map();
+ this._accounts = new Map();
+ this._highestOrdinal = 0;
+ }
+
+ get _accountKeys() {
+ let accountKeySet = new Set();
+ let branch = Services.prefs.getBranch(ACCOUNT_ROOT);
+ let children = branch.getChildList("");
+ for (let child of children) {
+ let subbranch = child.substr(0, child.indexOf("."));
+ accountKeySet.add(subbranch);
+
+ let match = /^account(\d+)$/.exec(subbranch);
+ if (match) {
+ let ordinal = parseInt(match[1], 10);
+ this._highestOrdinal = Math.max(this._highestOrdinal, ordinal);
+ }
+ }
+
+ // TODO: sort by ordinal
+ return accountKeySet.keys();
+ }
+
+ /**
+ * Ensure that we have the account key for an account. If we already have the
+ * key, just return it. If we have the account, get the key from it.
+ *
+ * @param aKeyOrAccount the key or the account object
+ * @returns the account key
+ */
+ _ensureKey(aKeyOrAccount) {
+ if (typeof aKeyOrAccount == "string") {
+ return aKeyOrAccount;
+ }
+ if ("accountKey" in aKeyOrAccount) {
+ return aKeyOrAccount.accountKey;
+ }
+ throw new Error("String or cloud file account expected");
+ }
+
+ /**
+ * Register a cloudfile provider, e.g. from an extension.
+ *
+ * @param {object} The implementation to register
+ */
+ registerProvider(aType, aProvider) {
+ if (this._providers.has(aType)) {
+ throw new Error(`Cloudfile provider ${aType} is already registered`);
+ }
+ this._providers.set(aType, aProvider);
+ this.emit("providerRegistered", aProvider);
+ }
+
+ /**
+ * Unregister a cloudfile provider.
+ *
+ * @param {string} aType - The provider type to unregister
+ */
+ unregisterProvider(aType) {
+ if (!this._providers.has(aType)) {
+ throw new Error(`Cloudfile provider ${aType} is not registered`);
+ }
+
+ for (let account of this.getAccountsForType(aType)) {
+ this._accounts.delete(account.accountKey);
+ }
+
+ this._providers.delete(aType);
+ this.emit("providerUnregistered", aType);
+ }
+
+ get providers() {
+ return [...this._providers.values()];
+ }
+
+ getProviderForType(aType) {
+ return this._providers.get(aType);
+ }
+
+ createAccount(aType) {
+ this._highestOrdinal++;
+ let key = "account" + this._highestOrdinal;
+
+ try {
+ let provider = this.getProviderForType(aType);
+ let account = provider.initAccount(key);
+
+ Services.prefs.setCharPref(ACCOUNT_ROOT + key + ".type", aType);
+ Services.prefs.setCharPref(
+ ACCOUNT_ROOT + key + ".displayName",
+ account.displayName
+ );
+
+ this._accounts.set(key, account);
+ this.emit("accountAdded", account);
+ return account;
+ } catch (e) {
+ for (let prefName of Services.prefs.getChildList(
+ `${ACCOUNT_ROOT}${key}.`
+ )) {
+ Services.prefs.clearUserPref(prefName);
+ }
+ throw e;
+ }
+ }
+
+ removeAccount(aKeyOrAccount) {
+ let key = this._ensureKey(aKeyOrAccount);
+ let type = Services.prefs.getCharPref(ACCOUNT_ROOT + key + ".type");
+
+ this._accounts.delete(key);
+ for (let prefName of Services.prefs.getChildList(
+ `${ACCOUNT_ROOT}${key}.`
+ )) {
+ Services.prefs.clearUserPref(prefName);
+ }
+
+ this.emit("accountDeleted", key, type);
+ }
+
+ get accounts() {
+ let arr = [];
+ for (let key of this._accountKeys) {
+ let account = this.getAccount(key);
+ if (account) {
+ arr.push(account);
+ }
+ }
+ return arr;
+ }
+
+ get configuredAccounts() {
+ return this.accounts.filter(account => account.configured);
+ }
+
+ getAccount(aKey) {
+ if (this._accounts.has(aKey)) {
+ return this._accounts.get(aKey);
+ }
+
+ let type = Services.prefs.getCharPref(ACCOUNT_ROOT + aKey + ".type", "");
+ if (type) {
+ let provider = this.getProviderForType(type);
+ if (provider) {
+ let account = provider.initAccount(aKey);
+ this._accounts.set(aKey, account);
+ return account;
+ }
+ }
+ return null;
+ }
+
+ getAccountsForType(aType) {
+ let result = [];
+
+ for (let accountKey of this._accountKeys) {
+ let type = Services.prefs.getCharPref(
+ ACCOUNT_ROOT + accountKey + ".type"
+ );
+ if (type === aType) {
+ result.push(this.getAccount(accountKey));
+ }
+ }
+
+ return result;
+ }
+
+ getDisplayName(aKeyOrAccount) {
+ // If no display name has been set, we return the empty string.
+ let key = this._ensureKey(aKeyOrAccount);
+ return Services.prefs.getCharPref(ACCOUNT_ROOT + key + ".displayName", "");
+ }
+
+ setDisplayName(aKeyOrAccount, aDisplayName) {
+ let key = this._ensureKey(aKeyOrAccount);
+ Services.prefs.setCharPref(
+ ACCOUNT_ROOT + key + ".displayName",
+ aDisplayName
+ );
+ }
+})();
diff --git a/comm/mail/components/cloudfile/content/selectDialog.js b/comm/mail/components/cloudfile/content/selectDialog.js
new file mode 100644
index 0000000000..4c49d11aa3
--- /dev/null
+++ b/comm/mail/components/cloudfile/content/selectDialog.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../../../../../toolkit/components/prompts/content/selectDialog.js */
+
+function cloudfileDialogOnLoad() {
+ let icons = propBag.getProperty("icons");
+ let listItems = listBox.itemChildren;
+ for (let i = 0; i < listItems.length; i++) {
+ listItems[i].setAttribute("align", "center");
+ let image = document.createElement("img");
+ image.setAttribute("src", icons[i]);
+ image.setAttribute("alt", "");
+ listItems[i].insertBefore(image, listItems[i].firstElementChild);
+ }
+}
diff --git a/comm/mail/components/cloudfile/content/selectDialog.xhtml b/comm/mail/components/cloudfile/content/selectDialog.xhtml
new file mode 100644
index 0000000000..3f99fea350
--- /dev/null
+++ b/comm/mail/components/cloudfile/content/selectDialog.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/cloudfileSelectDialog.css" type="text/css"?>
+<!DOCTYPE window>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="cloudfileDialogOnLoad();"
+>
+ <dialog>
+ <script
+ type="application/javascript"
+ src="chrome://messenger/content/cloudfile/selectDialog.js"
+ />
+ <script
+ type="application/javascript"
+ src="chrome://global/content/selectDialog.js"
+ />
+ <keyset id="dialogKeys" />
+ <vbox style="width: 24em; margin: 5px">
+ <label id="info.txt" />
+ <vbox>
+ <richlistbox id="list" class="theme-listbox" style="height: 8em" />
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/cloudfile/jar.mn b/comm/mail/components/cloudfile/jar.mn
new file mode 100644
index 0000000000..ad4eeb3065
--- /dev/null
+++ b/comm/mail/components/cloudfile/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/cloudfile/selectDialog.js (content/selectDialog.js)
+ content/messenger/cloudfile/selectDialog.xhtml (content/selectDialog.xhtml)
diff --git a/comm/mail/components/cloudfile/moz.build b/comm/mail/components/cloudfile/moz.build
new file mode 100644
index 0000000000..f456f7ad85
--- /dev/null
+++ b/comm/mail/components/cloudfile/moz.build
@@ -0,0 +1,14 @@
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "cloudFileAccounts.jsm",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.ini",
+]
diff --git a/comm/mail/components/cloudfile/test/browser/browser.ini b/comm/mail/components/cloudfile/test/browser/browser.ini
new file mode 100644
index 0000000000..8f4b1a954a
--- /dev/null
+++ b/comm/mail/components/cloudfile/test/browser/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+head = head.js
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+subsuite = thunderbird
+support-files = files/icon.svg files/management.html
+
+[browser_repeat_upload.js]
+support-files = files/green_eggs.txt
diff --git a/comm/mail/components/cloudfile/test/browser/browser_repeat_upload.js b/comm/mail/components/cloudfile/test/browser/browser_repeat_upload.js
new file mode 100644
index 0000000000..7390354a8c
--- /dev/null
+++ b/comm/mail/components/cloudfile/test/browser/browser_repeat_upload.js
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../../../../base/content/mailWindowOverlay.js */
+
+let { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+
+const ICON_URL = getRootDirectory(gTestPath) + "files/icon.svg";
+const MANAGEMENT_URL = getRootDirectory(gTestPath) + "files/management.html";
+
+function getFileFromChromeURL(leafName) {
+ let ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+
+ let url = Services.io.newURI(
+ getRootDirectory(gTestPath) + "files/" + leafName
+ );
+ let fileURL = ChromeRegistry.convertChromeURL(url).QueryInterface(
+ Ci.nsIFileURL
+ );
+ return fileURL.file;
+}
+
+add_task(async () => {
+ let uploadedFiles = [];
+ let provider = {
+ type: "Mochitest",
+ displayName: "Mochitest",
+ iconURL: ICON_URL,
+ initAccount(accountKey) {
+ return {
+ accountKey,
+ type: "Mochitest",
+ get displayName() {
+ return Services.prefs.getCharPref(
+ `mail.cloud_files.accounts.${this.accountKey}.displayName`,
+ "Mochitest Account"
+ );
+ },
+ getPreviousUploads() {
+ return uploadedFiles;
+ },
+ urlForFile(file) {
+ return "https://mochi.test/" + file.leafName;
+ },
+ iconURL: ICON_URL,
+ configured: true,
+ managementURL: MANAGEMENT_URL,
+ reuseUploads: true,
+ };
+ },
+ };
+
+ Assert.equal(
+ cloudFileAccounts.configuredAccounts.length,
+ 0,
+ "Should have no cloudfile accounts starting off."
+ );
+
+ cloudFileAccounts.registerProvider(provider.type, provider);
+ let account = cloudFileAccounts.createAccount(provider.type);
+ Assert.equal(
+ cloudFileAccounts.configuredAccounts.length,
+ 1,
+ "Should have only the one account we created."
+ );
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ MsgNewMessage();
+ let composeWindow = await composeWindowPromise;
+ await BrowserTestUtils.waitForEvent(composeWindow, "compose-editor-ready");
+ await TestUtils.waitForCondition(
+ () => Services.focus.activeWindow == composeWindow
+ );
+ let composeDocument = composeWindow.document;
+
+ // Compose window loaded.
+ // Check the attach dropdown has our account as a <menuitem>.
+
+ let toolbarButton = composeDocument.getElementById("button-attach");
+ let rect = toolbarButton.getBoundingClientRect();
+ EventUtils.synthesizeMouse(
+ toolbarButton,
+ rect.width - 5,
+ 5,
+ { clickCount: 1 },
+ composeWindow
+ );
+ await promiseAnimationFrame(composeWindow);
+
+ let menu = composeDocument.getElementById(
+ "button-attachPopup_attachCloudMenu"
+ );
+ ok(!BrowserTestUtils.is_hidden(menu));
+
+ let popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(menu, { clickCount: 1 }, composeWindow);
+ await popupshown;
+
+ Assert.equal(
+ cloudFileAccounts.configuredAccounts.length,
+ 1,
+ "Should still have one registered account."
+ );
+
+ let menuitems = menu.menupopup.children;
+ is(menuitems.length, 1);
+ is(menuitems[0].getAttribute("image"), ICON_URL);
+ is(menuitems[0].getAttribute("label"), "Mochitest Account\u2026");
+
+ composeDocument.getElementById("button-attachPopup").hidePopup();
+
+ // Pretend we uploaded some files before.
+
+ uploadedFiles = [
+ {
+ id: 1,
+ name: "green_eggs.txt",
+ path: getFileFromChromeURL("green_eggs.txt").path,
+ size: 30,
+ url: "https://mochi.test/green_eggs.txt",
+ serviceName: "MyCloud",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ },
+ {
+ id: 2,
+ name: "ham.zip",
+ path: getFileFromChromeURL("ham.zip").path,
+ size: 1234,
+ url: "https://mochi.test/ham.zip",
+ },
+ ];
+ is(account.getPreviousUploads().length, 2);
+
+ // Check the attach dropdown has our account as a <menu>.
+
+ await new Promise(resolve => {
+ toolbarButton.addEventListener("popupshown", resolve, { once: true });
+ EventUtils.synthesizeMouse(
+ toolbarButton,
+ rect.width - 5,
+ 5,
+ { clickCount: 1 },
+ composeWindow
+ );
+ });
+ info("toolbar button menu opened");
+ await promiseAnimationFrame(composeWindow);
+
+ await new Promise(resolve => {
+ menu.menupopup.addEventListener("popupshown", resolve, { once: true });
+ EventUtils.synthesizeMouseAtCenter(menu, { clickCount: 1 }, composeWindow);
+ });
+ info("file link menu opened");
+ await promiseAnimationFrame(composeWindow);
+
+ menuitems = menu.menupopup.children;
+ is(menuitems.length, 2);
+ is(menuitems[0].getAttribute("image"), ICON_URL);
+ is(menuitems[0].getAttribute("label"), "Mochitest Account\u2026");
+ is(menuitems[1].localName, "menuitem");
+ is(menuitems[1].getAttribute("image"), "moz-icon://green_eggs.txt");
+ is(menuitems[1].getAttribute("label"), "green_eggs.txt");
+ // TODO: Enable this when we handle files that no longer exist on the filesystem.
+ // is(menuitems[2].localName, "menuitem");
+ // is(menuitems[2].getAttribute("image"), "moz-icon://ham.zip");
+ // is(menuitems[2].getAttribute("label"), "ham.zip");
+
+ // Select one of the previously-uploaded items and check the attachment is added.
+
+ let bucket = composeDocument.getElementById("attachmentBucket");
+ await new Promise(resolve => {
+ bucket.addEventListener("attachments-added", resolve, { once: true });
+ menu.menupopup.activateItem(menuitems[1]);
+ });
+ info("attachment added");
+ await promiseAnimationFrame(composeWindow);
+ ok(toolbarButton.open === false);
+
+ is(bucket.itemCount, 1);
+ let attachment = bucket.itemChildren[0];
+ is(attachment.getAttribute("name"), "green_eggs.txt");
+ ok(attachment.attachment.sendViaCloud);
+ is(attachment.attachment.cloudFileAccountKey, account.accountKey);
+ is(
+ attachment.attachment.contentLocation,
+ "https://mochi.test/green_eggs.txt"
+ );
+
+ is(
+ attachment.querySelector("img.attachmentcell-icon").src,
+ uploadedFiles[0].serviceIcon,
+ "CloudFile icon should be correct."
+ );
+
+ // Check the content of the editor for the added template.
+ let editor = composeWindow.GetCurrentEditor();
+ let urls = editor.document.querySelectorAll(
+ "body > #cloudAttachmentListRoot > #cloudAttachmentList"
+ );
+ Assert.equal(urls.length, 1, "Found 1 FileLink template in the document.");
+
+ // Template is added asynchronously.
+ await TestUtils.waitForCondition(() => urls[0].querySelector("li"));
+ Assert.equal(
+ urls[0].querySelector(".cloudfile-name").textContent,
+ "green_eggs.txt",
+ "The name of the cloud file in the template should be correct."
+ );
+
+ Assert.equal(
+ urls[0].querySelector(".cloudfile-name").href,
+ "https://mochi.test/green_eggs.txt",
+ "The URL attached to the name of the cloud file in the template should be correct."
+ );
+
+ Assert.equal(
+ urls[0].querySelector(".cloudfile-service-name").textContent,
+ "MyCloud",
+ "The used service name in the template should be correct."
+ );
+
+ Assert.equal(
+ urls[0].querySelector(".cloudfile-service-icon").src,
+ "data:image/svg+xml;filename=globe.svg;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgZD0iTTggMGE4IDggMCAxIDAgOCA4IDguMDA5IDguMDA5IDAgMCAwLTgtOHptNS4xNjMgNC45NThoLTEuNTUyYTcuNyA3LjcgMCAwIDAtMS4wNTEtMi4zNzYgNi4wMyA2LjAzIDAgMCAxIDIuNjAzIDIuMzc2ek0xNCA4YTUuOTYzIDUuOTYzIDAgMCAxLS4zMzUgMS45NThoLTEuODIxQTEyLjMyNyAxMi4zMjcgMCAwIDAgMTIgOGExMi4zMjcgMTIuMzI3IDAgMCAwLS4xNTYtMS45NThoMS44MjFBNS45NjMgNS45NjMgMCAwIDEgMTQgOHptLTYgNmMtMS4wNzUgMC0yLjAzNy0xLjItMi41NjctMi45NThoNS4xMzVDMTAuMDM3IDEyLjggOS4wNzUgMTQgOCAxNHpNNS4xNzQgOS45NThhMTEuMDg0IDExLjA4NCAwIDAgMSAwLTMuOTE2aDUuNjUxQTExLjExNCAxMS4xMTQgMCAwIDEgMTEgOGExMS4xMTQgMTEuMTE0IDAgMCAxLS4xNzQgMS45NTh6TTIgOGE1Ljk2MyA1Ljk2MyAwIDAgMSAuMzM1LTEuOTU4aDEuODIxYTEyLjM2MSAxMi4zNjEgMCAwIDAgMCAzLjkxNkgyLjMzNUE1Ljk2MyA1Ljk2MyAwIDAgMSAyIDh6bTYtNmMxLjA3NSAwIDIuMDM3IDEuMiAyLjU2NyAyLjk1OEg1LjQzM0M1Ljk2MyAzLjIgNi45MjUgMiA4IDJ6bS0yLjU2LjU4MmE3LjcgNy43IDAgMCAwLTEuMDUxIDIuMzc2SDIuODM3QTYuMDMgNi4wMyAwIDAgMSA1LjQ0IDIuNTgyem0tMi42IDguNDZoMS41NDlhNy43IDcuNyAwIDAgMCAxLjA1MSAyLjM3NiA2LjAzIDYuMDMgMCAwIDEtMi42MDMtMi4zNzZ6bTcuNzIzIDIuMzc2YTcuNyA3LjcgMCAwIDAgMS4wNTEtMi4zNzZoMS41NTJhNi4wMyA2LjAzIDAgMCAxLTIuNjA2IDIuMzc2eiI+PC9wYXRoPgo8L3N2Zz4K",
+ "The used service icon should be correct."
+ );
+
+ // clean up
+ cloudFileAccounts.removeAccount(account);
+ cloudFileAccounts.unregisterProvider(provider.type);
+ Assert.equal(
+ cloudFileAccounts.configuredAccounts.length,
+ 0,
+ "Should leave no cloudfile accounts when done"
+ );
+ composeWindow.close();
+
+ // Request focus on something in the main window so the test doesn't time
+ // out waiting for focus.
+ document.getElementById("button-appmenu").focus();
+});
diff --git a/comm/mail/components/cloudfile/test/browser/files/green_eggs.txt b/comm/mail/components/cloudfile/test/browser/files/green_eggs.txt
new file mode 100644
index 0000000000..058318befb
--- /dev/null
+++ b/comm/mail/components/cloudfile/test/browser/files/green_eggs.txt
@@ -0,0 +1 @@
+I do not like them, Sam I Am!
diff --git a/comm/mail/components/cloudfile/test/browser/files/icon.svg b/comm/mail/components/cloudfile/test/browser/files/icon.svg
new file mode 100644
index 0000000000..6c1a552445
--- /dev/null
+++ b/comm/mail/components/cloudfile/test/browser/files/icon.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <circle cx="8" cy="8" r="7.5" fill="#ffffff" stroke="#00aa00" stroke-width="1.5"/>
+ <circle cx="5" cy="6" r="1.5" fill="#00aa00"/>
+ <circle cx="11" cy="6" r="1.5" fill="#00aa00"/>
+ <path d="M 12.83,9.30 C 12.24,11.48 10.26,13 8,13 5.75,13 3.74,11.48 3.17,9.29" fill="none" stroke="#00aa00" stroke-width="1.5"/>
+</svg>
diff --git a/comm/mail/components/cloudfile/test/browser/files/management.html b/comm/mail/components/cloudfile/test/browser/files/management.html
new file mode 100644
index 0000000000..5a51891fd7
--- /dev/null
+++ b/comm/mail/components/cloudfile/test/browser/files/management.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title></title>
+</head>
+<body>
+
+</body>
+</html>
diff --git a/comm/mail/components/cloudfile/test/browser/head.js b/comm/mail/components/cloudfile/test/browser/head.js
new file mode 100644
index 0000000000..3dd17de883
--- /dev/null
+++ b/comm/mail/components/cloudfile/test/browser/head.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_setup(async function () {
+ let gAccount = createAccount();
+ addIdentity(gAccount);
+ let rootFolder = gAccount.incomingServer.rootFolder;
+
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ about3Pane.displayFolder(rootFolder.URI);
+ await new Promise(resolve => executeSoon(resolve));
+});
+
+function createAccount() {
+ registerCleanupFunction(() => {
+ MailServices.accounts.accounts.forEach(cleanUpAccount);
+ });
+
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ info(`Created account ${account.toString()}`);
+
+ return account;
+}
+
+function cleanUpAccount(account) {
+ info(`Cleaning up account ${account.toString()}`);
+ MailServices.accounts.removeAccount(account, true);
+}
+
+function addIdentity(account) {
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = "mochitest@localhost";
+ account.addIdentity(identity);
+ account.defaultIdentity = identity;
+ info(`Created identity ${identity.toString()}`);
+}
+
+async function promiseAnimationFrame(win = window) {
+ await new Promise(win.requestAnimationFrame);
+ // dispatchToMainThread throws if used as the first argument of Promise.
+ return new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+}