summaryrefslogtreecommitdiffstats
path: root/comm/chat/modules/test
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/modules/test')
-rw-r--r--comm/chat/modules/test/test_InteractiveBrowser.js280
-rw-r--r--comm/chat/modules/test/test_NormalizedMap.js80
-rw-r--r--comm/chat/modules/test/test_filtering.js479
-rw-r--r--comm/chat/modules/test/test_imThemes.js342
-rw-r--r--comm/chat/modules/test/test_jsProtoHelper.js159
-rw-r--r--comm/chat/modules/test/test_otrlib.js21
-rw-r--r--comm/chat/modules/test/xpcshell.ini10
7 files changed, 1371 insertions, 0 deletions
diff --git a/comm/chat/modules/test/test_InteractiveBrowser.js b/comm/chat/modules/test/test_InteractiveBrowser.js
new file mode 100644
index 0000000000..eb39d7048b
--- /dev/null
+++ b/comm/chat/modules/test/test_InteractiveBrowser.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { InteractiveBrowser, CancelledError } = ChromeUtils.importESModule(
+ "resource:///modules/InteractiveBrowser.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_task(async function test_waitForRedirectOnLocationChange() {
+ const url = "https://example.com";
+ const promptText = "lorem ipsum";
+ const { window, webProgress } = getRequestStubs();
+
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ let resolved = false;
+ const request = InteractiveBrowser.waitForRedirect(url, promptText).then(
+ redirectUrl => {
+ resolved = true;
+ return redirectUrl;
+ }
+ );
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.loaded(window, webProgress);
+ await TestUtils.waitForTick();
+ ok(webProgress.listener, "Progress listener added");
+ equal(window.document.title, promptText, "Window title set");
+
+ const intermediate = "https://intermediate.example.com/";
+ webProgress.listener.onLocationChange(
+ webProgress,
+ {
+ name: intermediate + 1,
+ },
+ {
+ spec: intermediate + 1,
+ }
+ );
+ ok(
+ webProgress.listener,
+ "Progress listener still there after intermediary redirect"
+ );
+ ok(!resolved, "Still waiting for redirect");
+ webProgress.listener.onStateChange(
+ webProgress,
+ {
+ name: intermediate + 2,
+ },
+ Ci.nsIWebProgressListener.STATE_START,
+ null
+ );
+ ok(webProgress.listener, "Listener still there after second redirect");
+ ok(!resolved, "Still waiting for redirect 2");
+
+ const completionUrl = InteractiveBrowser.COMPLETION_URL + "/test?code=asdf";
+ webProgress.listener.onLocationChange(
+ webProgress,
+ {
+ name: completionUrl,
+ },
+ {
+ spec: completionUrl,
+ }
+ );
+
+ const redirectedUrl = await request;
+ ok(resolved, "Redirect complete");
+ equal(redirectedUrl, completionUrl);
+
+ ok(!webProgress.listener);
+ ok(window.closed);
+});
+
+add_task(async function test_waitForRedirectOnStateChangeStart() {
+ const url = "https://example.com";
+ const promptText = "lorem ipsum";
+ const { window, webProgress } = getRequestStubs();
+
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ let resolved = false;
+ const request = InteractiveBrowser.waitForRedirect(url, promptText).then(
+ redirectUrl => {
+ resolved = true;
+ return redirectUrl;
+ }
+ );
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.loaded(window, webProgress);
+ await TestUtils.waitForTick();
+ ok(webProgress.listener, "Progress listener added");
+ equal(window.document.title, promptText, "Window title set");
+
+ const intermediate = "https://intermediate.example.com/";
+ webProgress.listener.onStateChange(
+ webProgress,
+ {
+ name: intermediate,
+ },
+ Ci.nsIWebProgressListener.STATE_START,
+ null
+ );
+ ok(webProgress.listener);
+ ok(!resolved);
+
+ const completionUrl = InteractiveBrowser.COMPLETION_URL + "/test?code=asdf";
+ webProgress.listener.onStateChange(
+ webProgress,
+ {
+ name: completionUrl,
+ },
+ Ci.nsIWebProgressListener.STATE_START
+ );
+
+ const redirectedUrl = await request;
+ ok(resolved, "Redirect complete");
+ equal(redirectedUrl, completionUrl);
+
+ ok(!webProgress.listener);
+ ok(window.closed);
+});
+
+add_task(async function test_waitForRedirectOnStateChangeStart() {
+ const url = "https://example.com";
+ const promptText = "lorem ipsum";
+ const { window, webProgress } = getRequestStubs();
+
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ let resolved = false;
+ const request = InteractiveBrowser.waitForRedirect(url, promptText).then(
+ redirectUrl => {
+ resolved = true;
+ return redirectUrl;
+ }
+ );
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.loaded(window, webProgress);
+ await TestUtils.waitForTick();
+ ok(webProgress.listener, "Progress listener added");
+ equal(window.document.title, promptText, "Window title set");
+
+ const intermediate = "https://intermediate.example.com/";
+ webProgress.listener.onStateChange(
+ webProgress,
+ {
+ name: intermediate,
+ },
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK,
+ null
+ );
+ ok(webProgress.listener);
+ ok(!resolved);
+
+ const completionUrl = InteractiveBrowser.COMPLETION_URL + "/test?code=asdf";
+ webProgress.listener.onStateChange(
+ webProgress,
+ {
+ name: completionUrl,
+ },
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK
+ );
+
+ const redirectedUrl = await request;
+ ok(resolved, "Redirect complete");
+ equal(redirectedUrl, completionUrl);
+
+ ok(!webProgress.listener);
+ ok(window.closed);
+});
+
+add_task(async function test_waitForRedirectCancelled() {
+ const url = "https://example.com";
+ const promptText = "lorem ipsum";
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ const request = InteractiveBrowser.waitForRedirect(url, promptText);
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.cancelled();
+
+ await rejects(request, CancelledError);
+});
+
+add_task(async function test_waitForRedirectImmediatelyAborted() {
+ const url = "https://example.com";
+ const promptText = "lorem ipsum";
+ const { window, webProgress } = getRequestStubs();
+
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ const request = InteractiveBrowser.waitForRedirect(url, promptText);
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.loaded(window, webProgress);
+ subject.wrappedJSObject.cancelled();
+ await TestUtils.waitForTick();
+ ok(!webProgress.listener);
+
+ await rejects(request, CancelledError);
+});
+
+add_task(async function test_waitForRedirectAbortEvent() {
+ const url = "https://example.com";
+ const promptText = "lorem ipsum";
+ const { window, webProgress } = getRequestStubs();
+
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ const request = InteractiveBrowser.waitForRedirect(url, promptText);
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.loaded(window, webProgress);
+ await TestUtils.waitForTick();
+ ok(webProgress.listener);
+ equal(window.document.title, promptText);
+
+ subject.wrappedJSObject.cancelled();
+ await rejects(request, CancelledError);
+ ok(!webProgress.listener);
+ ok(window.closed);
+});
+
+add_task(async function test_waitForRedirectAlreadyArrived() {
+ const url = "https://example.com";
+ const completionUrl = InteractiveBrowser.COMPLETION_URL + "/test?code=asdf";
+ const promptText = "lorem ipsum";
+ const { window, webProgress } = getRequestStubs();
+ window.initialURI = completionUrl;
+
+ const observeTopic = TestUtils.topicObserved("browser-request");
+ let resolved = false;
+ const request = InteractiveBrowser.waitForRedirect(url, promptText).then(
+ redirectUrl => {
+ resolved = true;
+ return redirectUrl;
+ }
+ );
+ const [subject] = await observeTopic;
+
+ subject.wrappedJSObject.loaded(window, webProgress);
+ const redirectedUrl = await request;
+
+ equal(window.document.title, promptText, "Window title set");
+ ok(resolved, "Redirect complete");
+ equal(redirectedUrl, completionUrl);
+
+ ok(!webProgress.listener);
+ ok(window.closed);
+});
+
+function getRequestStubs() {
+ const mocks = {
+ window: {
+ close() {
+ this.closed = true;
+ },
+ document: {
+ getElementById() {
+ return {
+ currentURI: {
+ spec: mocks.window.initialURI,
+ },
+ };
+ },
+ },
+ initialURI: "",
+ },
+ webProgress: {
+ addProgressListener(listener) {
+ this.listener = listener;
+ },
+ removeProgressListener(listener) {
+ if (this.listener === listener) {
+ delete this.listener;
+ }
+ },
+ },
+ };
+ return mocks;
+}
diff --git a/comm/chat/modules/test/test_NormalizedMap.js b/comm/chat/modules/test/test_NormalizedMap.js
new file mode 100644
index 0000000000..cad5bcd4d8
--- /dev/null
+++ b/comm/chat/modules/test/test_NormalizedMap.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { NormalizedMap } = ChromeUtils.importESModule(
+ "resource:///modules/NormalizedMap.sys.mjs"
+);
+
+function test_setter_getter() {
+ let m = new NormalizedMap(aStr => aStr.toLowerCase());
+ m.set("foo", "bar");
+ m.set("BaZ", "blah");
+ Assert.equal(m.has("FOO"), true);
+ Assert.equal(m.has("BaZ"), true);
+ Assert.equal(m.get("FOO"), "bar");
+
+ let keys = Array.from(m.keys());
+ Assert.equal(keys[0], "foo");
+ Assert.equal(keys[1], "baz");
+
+ let values = Array.from(m.values());
+ Assert.equal(values[0], "bar");
+ Assert.equal(values[1], "blah");
+
+ Assert.equal(m.size, 2);
+
+ run_next_test();
+}
+
+function test_constructor() {
+ let k = new NormalizedMap(
+ aStr => aStr.toLowerCase(),
+ [
+ ["A", 2],
+ ["b", 3],
+ ]
+ );
+ Assert.equal(k.get("b"), 3);
+ Assert.equal(k.get("a"), 2);
+ Assert.equal(k.get("B"), 3);
+ Assert.equal(k.get("A"), 2);
+
+ run_next_test();
+}
+
+function test_iterator() {
+ let k = new NormalizedMap(aStr => aStr.toLowerCase());
+ k.set("FoO", "bar");
+
+ for (let [key, value] of k) {
+ Assert.equal(key, "foo");
+ Assert.equal(value, "bar");
+ }
+
+ run_next_test();
+}
+
+function test_delete() {
+ let m = new NormalizedMap(aStr => aStr.toLowerCase());
+ m.set("foo", "bar");
+ m.set("BaZ", "blah");
+
+ Assert.equal(m.delete("blah"), false);
+
+ Assert.equal(m.delete("FOO"), true);
+ Assert.equal(m.size, 1);
+
+ Assert.equal(m.delete("baz"), true);
+ Assert.equal(m.size, 0);
+
+ run_next_test();
+}
+
+function run_test() {
+ add_test(test_setter_getter);
+ add_test(test_constructor);
+ add_test(test_iterator);
+ add_test(test_delete);
+
+ run_next_test();
+}
diff --git a/comm/chat/modules/test/test_filtering.js b/comm/chat/modules/test/test_filtering.js
new file mode 100644
index 0000000000..33c8fcf262
--- /dev/null
+++ b/comm/chat/modules/test/test_filtering.js
@@ -0,0 +1,479 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// These tests run into issues if there isn't a profile directory, see bug 1542397.
+do_get_profile();
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+var {
+ cleanupImMarkup,
+ createDerivedRuleset,
+ addGlobalAllowedTag,
+ removeGlobalAllowedTag,
+ addGlobalAllowedAttribute,
+ removeGlobalAllowedAttribute,
+ addGlobalAllowedStyleRule,
+ removeGlobalAllowedStyleRule,
+} = ChromeUtils.importESModule("resource:///modules/imContentSink.sys.mjs");
+
+var kModePref = "messenger.options.filterMode";
+var kStrictMode = 0,
+ kStandardMode = 1,
+ kPermissiveMode = 2;
+
+function run_test() {
+ let defaultMode = Services.prefs.getIntPref(kModePref);
+
+ add_test(test_strictMode);
+ add_test(test_standardMode);
+ add_test(test_permissiveMode);
+ add_test(test_addGlobalAllowedTag);
+ add_test(test_addGlobalAllowedAttribute);
+ add_test(test_addGlobalAllowedStyleRule);
+ add_test(test_createDerivedRuleset);
+
+ Services.prefs.setIntPref(kModePref, defaultMode);
+ run_next_test();
+}
+
+// Sanity check: a string without HTML markup shouldn't be modified.
+function test_plainText() {
+ const strings = [
+ "foo",
+ "foo ", // preserve trailing whitespace
+ " foo", // preserve leading indent
+ "<html>&", // keep escaped characters
+ ];
+ for (let string of strings) {
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+}
+
+function test_paragraphs() {
+ const strings = ["<p>foo</p><p>bar</p>", "<p>foo<br>bar</p>", "foo<br>bar"];
+ for (let string of strings) {
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+}
+
+function test_stripScripts() {
+ const strings = [
+ ["<script>alert('hey')</script>", ""],
+ ["foo <script>alert('hey')</script>", "foo "],
+ ["<p onclick=\"alert('hey')\">foo</p>", "<p>foo</p>"],
+ ["<p onmouseover=\"alert('hey')\">foo</p>", "<p>foo</p>"],
+ ];
+ for (let [input, expectedOutput] of strings) {
+ Assert.equal(expectedOutput, cleanupImMarkup(input));
+ }
+}
+
+function test_links() {
+ // http, https, ftp and mailto links should be preserved.
+ const ok = [
+ "http://example.com/",
+ "https://example.com/",
+ "ftp://example.com/",
+ "mailto:foo@example.com",
+ ];
+ for (let string of ok) {
+ string = '<a href="' + string + '">foo</a>';
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+
+ // other links should be removed
+ const bad = [
+ "chrome://global/content/",
+ "about:",
+ "about:blank",
+ "foo://bar/",
+ "",
+ ];
+ for (let string of bad) {
+ Assert.equal(
+ "<a>foo</a>",
+ cleanupImMarkup('<a href="' + string + '">foo</a>')
+ );
+ }
+
+ // keep link titles
+ let string = '<a title="foo bar">foo</a>';
+ Assert.equal(string, cleanupImMarkup(string));
+}
+
+function test_table() {
+ const table =
+ "<table>" +
+ "<caption>test table</caption>" +
+ "<thead>" +
+ "<tr>" +
+ "<th>key</th>" +
+ "<th>data</th>" +
+ "</tr>" +
+ "</thead>" +
+ "<tbody>" +
+ "<tr>" +
+ "<td>lorem</td>" +
+ "<td>ipsum</td>" +
+ "</tr>" +
+ "</tbody>" +
+ "</table>";
+ Assert.equal(table, cleanupImMarkup(table));
+}
+
+function test_allModes() {
+ test_plainText();
+ test_paragraphs();
+ test_stripScripts();
+ test_links();
+ // Remove random classes.
+ Assert.equal("<p>foo</p>", cleanupImMarkup('<p class="foobar">foo</p>'));
+ // Test unparsable style.
+ Assert.equal("<p>foo</p>", cleanupImMarkup('<p style="not-valid">foo</p>'));
+}
+
+function test_strictMode() {
+ Services.prefs.setIntPref(kModePref, kStrictMode);
+ test_allModes();
+
+ // check that basic formatting is stripped in strict mode.
+ for (let tag of [
+ "div",
+ "em",
+ "strong",
+ "b",
+ "i",
+ "u",
+ "s",
+ "span",
+ "code",
+ "ul",
+ "li",
+ "ol",
+ "cite",
+ "blockquote",
+ "del",
+ "strike",
+ "ins",
+ "sub",
+ "sup",
+ "pre",
+ "td",
+ "details",
+ "h1",
+ ]) {
+ Assert.equal("foo", cleanupImMarkup("<" + tag + ">foo</" + tag + ">"));
+ }
+
+ // check that font settings are removed.
+ Assert.equal(
+ "foo",
+ cleanupImMarkup('<font face="Times" color="pink">foo</font>')
+ );
+ Assert.equal(
+ "<p>foo</p>",
+ cleanupImMarkup('<p style="font-weight: bold;">foo</p>')
+ );
+
+ // Discard hr
+ Assert.equal("foobar", cleanupImMarkup("foo<hr>bar"));
+
+ run_next_test();
+}
+
+function test_standardMode() {
+ Services.prefs.setIntPref(kModePref, kStandardMode);
+ test_allModes();
+ test_table();
+
+ // check that basic formatting is kept in standard mode.
+ for (let tag of [
+ "div",
+ "em",
+ "strong",
+ "b",
+ "i",
+ "u",
+ "s",
+ "span",
+ "code",
+ "ul",
+ "li",
+ "ol",
+ "cite",
+ "blockquote",
+ "del",
+ "sub",
+ "sup",
+ "pre",
+ "strike",
+ "ins",
+ "details",
+ ]) {
+ let string = "<" + tag + ">foo</" + tag + ">";
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+
+ // Keep special allowed classes.
+ for (let className of ["moz-txt-underscore", "moz-txt-tag"]) {
+ let string = '<span class="' + className + '">foo</span>';
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+
+ // Remove font settings
+ let font_string = '<font face="Times" color="pink" size="3">foo</font>';
+ Assert.equal("foo", cleanupImMarkup(font_string));
+
+ // Discard hr
+ Assert.equal("foobar", cleanupImMarkup("foo<hr>bar"));
+
+ const okCSS = ["font-style: italic", "font-weight: bold"];
+ for (let css of okCSS) {
+ let string = '<span style="' + css + '">foo</span>';
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+ // text-decoration is a shorthand for several text-decoration properties, but
+ // standard mode only allows text-decoration-line.
+ Assert.equal(
+ '<span style="text-decoration-line: underline;">foo</span>',
+ cleanupImMarkup('<span style="text-decoration: underline">foo</span>')
+ );
+
+ const badCSS = [
+ "color: pink;",
+ "font-family: Times",
+ "font-size: larger",
+ "display: none",
+ "visibility: hidden",
+ "unsupported-by-gecko: blah",
+ ];
+ for (let css of badCSS) {
+ Assert.equal(
+ "<span>foo</span>",
+ cleanupImMarkup('<span style="' + css + '">foo</span>')
+ );
+ }
+ // The shorthand 'font' is decomposed to non-shorthand properties,
+ // and not recomposed as some non-shorthand properties are filtered out.
+ Assert.equal(
+ '<span style="font-style: normal; font-weight: normal;">foo</span>',
+ cleanupImMarkup('<span style="font: 15px normal">foo</span>')
+ );
+
+ // Discard headings
+ const heading1 = "test heading";
+ Assert.equal(heading1, cleanupImMarkup(`<h1>${heading1}</h1>`));
+
+ // Setting the start number of an <ol> is allowed
+ const olWithOffset = '<ol start="2"><li>two</li><li>three</li></ol>';
+ Assert.equal(olWithOffset, cleanupImMarkup(olWithOffset));
+
+ run_next_test();
+}
+
+function test_permissiveMode() {
+ Services.prefs.setIntPref(kModePref, kPermissiveMode);
+ test_allModes();
+ test_table();
+
+ // Check that all formatting is kept in permissive mode.
+ for (let tag of [
+ "div",
+ "em",
+ "strong",
+ "b",
+ "i",
+ "u",
+ "span",
+ "code",
+ "ul",
+ "li",
+ "ol",
+ "cite",
+ "blockquote",
+ "del",
+ "sub",
+ "sup",
+ "pre",
+ "strike",
+ "ins",
+ "details",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ ]) {
+ let string = "<" + tag + ">foo</" + tag + ">";
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+
+ // Keep special allowed classes.
+ for (let className of ["moz-txt-underscore", "moz-txt-tag"]) {
+ let string = '<span class="' + className + '">foo</span>';
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+
+ // Keep font settings
+ const fontAttributes = ['face="Times"', 'color="pink"', 'size="3"'];
+ for (let fontAttribute of fontAttributes) {
+ let string = "<font " + fontAttribute + ">foo</font>";
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+
+ // Allow hr
+ let hr_string = "foo<hr>bar";
+ Assert.equal(hr_string, cleanupImMarkup(hr_string));
+
+ // Allow most CSS rules changing the text appearance.
+ const okCSS = [
+ "font-style: italic",
+ "font-weight: bold",
+ "color: pink;",
+ "font-family: Times",
+ "font-size: larger",
+ ];
+ for (let css of okCSS) {
+ let string = '<span style="' + css + '">foo</span>';
+ Assert.equal(string, cleanupImMarkup(string));
+ }
+ // text-decoration is a shorthand for several text-decoration properties, but
+ // permissive mode only allows text-decoration-color, text-decoration-line,
+ // and text-decoration-style.
+ Assert.equal(
+ '<span style="text-decoration-color: currentcolor; text-decoration-line: underline; text-decoration-style: solid;">foo</span>',
+ cleanupImMarkup('<span style="text-decoration: underline;">foo</span>')
+ );
+
+ // The shorthand 'font' is decomposed to non-shorthand properties,
+ // and not recomposed as some non-shorthand properties are filtered out.
+ Assert.equal(
+ '<span style="font-family: normal; font-size: 15px; ' +
+ 'font-style: normal; font-weight: normal;">foo</span>',
+ cleanupImMarkup('<span style="font: 15px normal">foo</span>')
+ );
+
+ // But still filter out dangerous CSS rules.
+ const badCSS = [
+ "display: none",
+ "visibility: hidden",
+ "unsupported-by-gecko: blah",
+ ];
+ for (let css of badCSS) {
+ Assert.equal(
+ "<span>foo</span>",
+ cleanupImMarkup('<span style="' + css + '">foo</span>')
+ );
+ }
+
+ run_next_test();
+}
+
+function test_addGlobalAllowedTag() {
+ Services.prefs.setIntPref(kModePref, kStrictMode);
+
+ // Check that <hr> isn't allowed by default in strict mode.
+ // Note: we use <hr> instead of <img> to avoid mailnews' content policy
+ // messing things up.
+ Assert.equal("", cleanupImMarkup("<hr>"));
+
+ // Allow <hr> without attributes.
+ addGlobalAllowedTag("hr");
+ Assert.equal("<hr>", cleanupImMarkup("<hr>"));
+ Assert.equal("<hr>", cleanupImMarkup('<hr src="http://example.com/">'));
+ removeGlobalAllowedTag("hr");
+
+ // Allow <hr> with an unfiltered src attribute.
+ addGlobalAllowedTag("hr", { src: true });
+ Assert.equal("<hr>", cleanupImMarkup('<hr alt="foo">'));
+ Assert.equal(
+ '<hr src="http://example.com/">',
+ cleanupImMarkup('<hr src="http://example.com/">')
+ );
+ Assert.equal(
+ '<hr src="chrome://global/skin/img.png">',
+ cleanupImMarkup('<hr src="chrome://global/skin/img.png">')
+ );
+ removeGlobalAllowedTag("hr");
+
+ // Allow <hr> with an src attribute taking only http(s) urls.
+ addGlobalAllowedTag("hr", { src: aValue => /^https?:/.test(aValue) });
+ Assert.equal(
+ '<hr src="http://example.com/">',
+ cleanupImMarkup('<hr src="http://example.com/">')
+ );
+ Assert.equal(
+ "<hr>",
+ cleanupImMarkup('<hr src="chrome://global/skin/img.png">')
+ );
+ removeGlobalAllowedTag("hr");
+
+ run_next_test();
+}
+
+function test_addGlobalAllowedAttribute() {
+ Services.prefs.setIntPref(kModePref, kStrictMode);
+
+ // Check that id isn't allowed by default in strict mode.
+ Assert.equal("<br>", cleanupImMarkup('<br id="foo">'));
+
+ // Allow id unconditionally.
+ addGlobalAllowedAttribute("id");
+ Assert.equal('<br id="foo">', cleanupImMarkup('<br id="foo">'));
+ removeGlobalAllowedAttribute("id");
+
+ // Allow id only with numbers.
+ addGlobalAllowedAttribute("id", aId => /^\d+$/.test(aId));
+ Assert.equal('<br id="123">', cleanupImMarkup('<br id="123">'));
+ Assert.equal("<br>", cleanupImMarkup('<br id="foo">'));
+ removeGlobalAllowedAttribute("id");
+
+ run_next_test();
+}
+
+function test_addGlobalAllowedStyleRule() {
+ // We need at least the standard mode to have the style attribute allowed.
+ Services.prefs.setIntPref(kModePref, kStandardMode);
+
+ // Check that clear isn't allowed by default in strict mode.
+ Assert.equal("<br>", cleanupImMarkup('<br style="clear: both;">'));
+
+ // Allow clear.
+ addGlobalAllowedStyleRule("clear");
+ Assert.equal(
+ '<br style="clear: both;">',
+ cleanupImMarkup('<br style="clear: both;">')
+ );
+ removeGlobalAllowedStyleRule("clear");
+
+ run_next_test();
+}
+
+function test_createDerivedRuleset() {
+ Services.prefs.setIntPref(kModePref, kStandardMode);
+
+ let rules = createDerivedRuleset();
+
+ let string = "<hr>";
+ Assert.equal("", cleanupImMarkup(string));
+ Assert.equal("", cleanupImMarkup(string, rules));
+ rules.tags.hr = true;
+ Assert.equal(string, cleanupImMarkup(string, rules));
+
+ string = '<br id="123">';
+ Assert.equal("<br>", cleanupImMarkup(string));
+ Assert.equal("<br>", cleanupImMarkup(string, rules));
+ rules.attrs.id = true;
+ Assert.equal(string, cleanupImMarkup(string, rules));
+
+ string = '<br style="clear: both;">';
+ Assert.equal("<br>", cleanupImMarkup(string));
+ Assert.equal("<br>", cleanupImMarkup(string, rules));
+ rules.styles.clear = true;
+ Assert.equal(string, cleanupImMarkup(string, rules));
+
+ run_next_test();
+}
diff --git a/comm/chat/modules/test/test_imThemes.js b/comm/chat/modules/test/test_imThemes.js
new file mode 100644
index 0000000000..61171fe121
--- /dev/null
+++ b/comm/chat/modules/test/test_imThemes.js
@@ -0,0 +1,342 @@
+/* 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 {
+ initHTMLDocument,
+ insertHTMLForMessage,
+ getHTMLForMessage,
+ replaceHTMLForMessage,
+ wasNextMessage,
+ removeMessage,
+ isNextMessage,
+} = ChromeUtils.importESModule("resource:///modules/imThemes.sys.mjs");
+const { MockDocument } = ChromeUtils.importESModule(
+ "resource://testing-common/MockDocument.sys.mjs"
+);
+
+const BASIC_CONV_DOCUMENT_HTML =
+ '<!DOCTYPE html><html><body><div id="Chat"></div></body></html>';
+
+add_task(function test_initHTMLDocument() {
+ const window = {};
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ "<!DOCTYPE html><html><head></head><body></body></html>"
+ );
+ Object.defineProperty(document, "defaultView", {
+ value: window,
+ });
+ const conversation = {
+ title: "test",
+ };
+ const theme = {
+ baseURI: "chrome://messenger-messagestyles/skin/test/",
+ variant: "default",
+ metadata: {},
+ html: {
+ footer: "",
+ script: 'console.log("hi");',
+ },
+ };
+ initHTMLDocument(conversation, theme, document);
+ equal(typeof document.defaultView.convertTimeUnits, "function");
+ equal(document.querySelector("base").href, theme.baseURI);
+ ok(
+ document.querySelector(
+ 'link[rel="stylesheet"][href="chrome://chat/skin/conv.css"]'
+ )
+ );
+ ok(document.querySelector('link[rel="stylesheet"][href="main.css"]'));
+
+ equal(document.body.id, "ibcontent");
+ ok(document.getElementById("Chat"));
+ equal(document.querySelector("script").src, theme.baseURI + "inline.js");
+});
+
+add_task(function test_insertHTMLForMessage() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = '<div style="background: blue;">foo bar</div>';
+ const message = {};
+ insertHTMLForMessage(message, html, document, false);
+ const messageElement = document.querySelector("#Chat > div");
+ strictEqual(messageElement._originalMsg, message);
+ equal(messageElement.style.backgroundColor, "blue");
+ equal(messageElement.textContent, "foo bar");
+ ok(!messageElement.dataset.isNext);
+});
+
+add_task(function test_insertHTMLForMessage_next() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = '<div style="background: blue;">foo bar</div>';
+ const message = {};
+ insertHTMLForMessage(message, html, document, true);
+ const messageElement = document.querySelector("#Chat > div");
+ strictEqual(messageElement._originalMsg, message);
+ equal(messageElement.style.backgroundColor, "blue");
+ equal(messageElement.textContent, "foo bar");
+ ok(messageElement.dataset.isNext);
+});
+
+add_task(function test_getHTMLForMessage() {
+ const message = {
+ incoming: true,
+ system: false,
+ message: "foo bar",
+ who: "userId",
+ alias: "display name",
+ color: "#ffbbff",
+ };
+ const theme = {
+ html: {
+ incomingContent:
+ '<span style="color: %senderColor%;">%sender%</span>%message%',
+ },
+ };
+ const html = getHTMLForMessage(message, theme, false, false);
+ equal(
+ html,
+ '<span style="color: #ffbbff;"><span class="ib-sender">display name</span></span><span class="ib-msg-txt">foo bar</span>'
+ );
+});
+
+add_task(function test_replaceHTMLForMessage() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = '<div style="background: blue;">foo bar</div>';
+ const message = {
+ remoteId: "foo",
+ };
+ insertHTMLForMessage(message, html, document, false);
+ const messageElement = document.querySelector("#Chat > div");
+ strictEqual(messageElement._originalMsg, message);
+ equal(messageElement.style.backgroundColor, "blue");
+ equal(messageElement.textContent, "foo bar");
+ equal(messageElement.dataset.remoteId, "foo");
+ ok(!messageElement.dataset.isNext);
+ const updatedHtml =
+ '<div style="background: green;">lorem ipsum</div><div id="insert"></div>';
+ const updatedMessage = {
+ remoteId: "foo",
+ };
+ replaceHTMLForMessage(updatedMessage, updatedHtml, document, true);
+ const updatedMessageElement = document.querySelector("#Chat > div");
+ strictEqual(updatedMessageElement._originalMsg, updatedMessage);
+ equal(updatedMessageElement.style.backgroundColor, "green");
+ equal(updatedMessageElement.textContent, "lorem ipsum");
+ equal(updatedMessageElement.dataset.remoteId, "foo");
+ ok(updatedMessageElement.dataset.isNext);
+ ok(
+ !document.querySelector("#insert"),
+ "Insert anchor in template is ignored when replacing"
+ );
+});
+
+add_task(function test_replaceHTMLForMessageWithoutExistingMessage() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const updatedHtml = '<div style="background: green;">lorem ipsum</div>';
+ const updatedMessage = {
+ remoteId: "foo",
+ };
+ replaceHTMLForMessage(updatedMessage, updatedHtml, document, false);
+ const updatedMessageElement = document.querySelector("#Chat > div");
+ ok(!updatedMessageElement);
+});
+
+add_task(function test_replaceHTMLForMessageWithoutRemoteId() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = '<div style="background: blue;">foo bar</div>';
+ const message = {
+ remoteId: "foo",
+ };
+ insertHTMLForMessage(message, html, document, false);
+ const messageElement = document.querySelector("#Chat > div");
+ strictEqual(messageElement._originalMsg, message);
+ equal(messageElement.style.backgroundColor, "blue");
+ equal(messageElement.textContent, "foo bar");
+ equal(messageElement.dataset.remoteId, "foo");
+ ok(!messageElement.dataset.isNext);
+ const updatedHtml = '<div style="background: green;">lorem ipsum</div>';
+ const updatedMessage = {};
+ replaceHTMLForMessage(updatedMessage, updatedHtml, document, false);
+ const updatedMessageElement = document.querySelector("#Chat > div");
+ strictEqual(updatedMessageElement._originalMsg, message);
+ equal(updatedMessageElement.style.backgroundColor, "blue");
+ equal(updatedMessageElement.textContent, "foo bar");
+ equal(updatedMessageElement.dataset.remoteId, "foo");
+ ok(!updatedMessageElement.dataset.isNext);
+});
+
+add_task(function test_wasNextMessage_isNext() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = "<div>foo bar</div>";
+ const message = {
+ remoteId: "foo",
+ };
+ insertHTMLForMessage(message, html, document, true);
+ ok(wasNextMessage(message, document));
+});
+
+add_task(function test_wasNextMessage_isNotNext() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = "<div>foo bar</div>";
+ const message = {
+ remoteId: "foo",
+ };
+ insertHTMLForMessage(message, html, document, false);
+ ok(!wasNextMessage(message, document));
+});
+
+add_task(function test_wasNextMessage_noPreviousVersion() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const message = {
+ remoteId: "foo",
+ };
+ ok(!wasNextMessage(message, document));
+});
+
+add_task(function test_removeMessage() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = '<div style="background: blue;">foo bar</div>';
+ const message = {
+ remoteId: "foo",
+ };
+ insertHTMLForMessage(message, html, document, false);
+ const messageElement = document.querySelector("#Chat > div");
+ strictEqual(messageElement._originalMsg, message);
+ equal(messageElement.style.backgroundColor, "blue");
+ equal(messageElement.textContent, "foo bar");
+ equal(messageElement.dataset.remoteId, "foo");
+ ok(!messageElement.dataset.isNext);
+ removeMessage("foo", document);
+ const messageElements = document.querySelectorAll("#Chat > div");
+ equal(messageElements.length, 0);
+});
+
+add_task(function test_removeMessage_noMatchingMessage() {
+ const document = MockDocument.createTestDocument(
+ "chrome://chat/content/conv.html",
+ BASIC_CONV_DOCUMENT_HTML
+ );
+ const html = '<div style="background: blue;">foo bar</div>';
+ const message = {
+ remoteId: "foo",
+ };
+ insertHTMLForMessage(message, html, document, false);
+ const messageElement = document.querySelector("#Chat > div");
+ strictEqual(messageElement._originalMsg, message);
+ equal(messageElement.style.backgroundColor, "blue");
+ equal(messageElement.textContent, "foo bar");
+ equal(messageElement.dataset.remoteId, "foo");
+ ok(!messageElement.dataset.isNext);
+ removeMessage("bar", document);
+ const messageElements = document.querySelectorAll("#Chat > div");
+ notEqual(messageElements.length, 0);
+});
+
+add_task(function test_isNextMessage() {
+ const theme = {
+ combineConsecutive: true,
+ metadata: {},
+ combineConsecutiveInterval: 300,
+ };
+ const messagePairs = [
+ {
+ message: {},
+ previousMessage: null,
+ isNext: false,
+ },
+ {
+ message: {
+ system: true,
+ },
+ previousMessage: {
+ system: true,
+ },
+ isNext: true,
+ },
+ {
+ message: {
+ who: "foo",
+ },
+ previousMessage: {
+ who: "bar",
+ },
+ isNext: false,
+ },
+ {
+ message: {
+ outgoing: true,
+ },
+ isNext: false,
+ },
+ {
+ message: {
+ incoming: true,
+ },
+ isNext: false,
+ },
+ {
+ message: {
+ system: true,
+ },
+ isNext: false,
+ },
+ {
+ message: {
+ time: 100,
+ },
+ previousMessage: {
+ time: 100,
+ },
+ isNext: true,
+ },
+ {
+ message: {
+ time: 300,
+ },
+ previousMessage: {
+ time: 100,
+ },
+ isNext: true,
+ },
+ {
+ message: {
+ time: 500,
+ },
+ previousMessage: {
+ time: 100,
+ },
+ isNext: false,
+ },
+ ];
+ for (const { message, previousMessage = {}, isNext } of messagePairs) {
+ equal(isNextMessage(theme, message, previousMessage), isNext);
+ }
+});
diff --git a/comm/chat/modules/test/test_jsProtoHelper.js b/comm/chat/modules/test/test_jsProtoHelper.js
new file mode 100644
index 0000000000..b87ec27241
--- /dev/null
+++ b/comm/chat/modules/test/test_jsProtoHelper.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { GenericConvIMPrototype } = ChromeUtils.importESModule(
+ "resource:///modules/jsProtoHelper.sys.mjs"
+);
+
+var _id = 0;
+function Conversation(name) {
+ this._name = name;
+ this._observers = [];
+ this._date = Date.now() * 1000;
+ this.id = ++_id;
+}
+Conversation.prototype = {
+ __proto__: GenericConvIMPrototype,
+ _account: {
+ imAccount: {
+ protocol: { name: "Fake Protocol" },
+ alias: "",
+ name: "Fake Account",
+ },
+ ERROR(e) {
+ throw e;
+ },
+ DEBUG() {},
+ },
+};
+
+// ROT13, used as an example transformation.
+function rot13(aString) {
+ return aString.replace(/[a-zA-Z]/g, function (c) {
+ return String.fromCharCode(
+ c.charCodeAt(0) + (c.toLowerCase() < "n" ? 1 : -1) * 13
+ );
+ });
+}
+
+// A test that cancels a message before it can be sent.
+add_task(function test_cancel_send_message() {
+ let conv = new Conversation();
+ conv.dispatchMessage = function (aMsg) {
+ ok(
+ false,
+ "The message should have been halted in the conversation service."
+ );
+ };
+
+ let sending = false;
+ conv.addObserver({
+ observe(aObject, aTopic, aMsg) {
+ switch (aTopic) {
+ case "sending-message":
+ ok(
+ aObject.QueryInterface(Ci.imIOutgoingMessage),
+ "Wrong message type."
+ );
+ aObject.cancelled = true;
+ sending = true;
+ break;
+ case "new-text":
+ ok(
+ false,
+ "No other notification should be fired for a cancelled message."
+ );
+ break;
+ }
+ },
+ });
+ conv.sendMsg("Hi!");
+ ok(sending, "The sending-message notification was never fired.");
+});
+
+// A test that ensures protocols get a chance to prepare a message before
+// sending and displaying.
+add_task(function test_prpl_message_prep() {
+ let conv = new Conversation();
+ conv.dispatchMessage = function (aMsg) {
+ this.writeMessage("user", aMsg, { outgoing: true });
+ };
+
+ conv.prepareForSending = function (aMsg) {
+ ok(aMsg.QueryInterface(Ci.imIOutgoingMessage), "Wrong message type.");
+ equal(aMsg.message, msg, "Expected the original message.");
+ prepared = true;
+ return [prefix + aMsg.message];
+ };
+
+ conv.prepareForDisplaying = function (aMsg) {
+ equal(aMsg.displayMessage, prefix + msg, "Expected the prefixed message.");
+ aMsg.displayMessage = aMsg.displayMessage.slice(prefix.length);
+ };
+
+ let msg = "Hi!";
+ let prefix = "test> ";
+
+ let prepared = false;
+ let receivedMsg = false;
+ conv.addObserver({
+ observe(aObject, aTopic) {
+ if (aTopic === "preparing-message") {
+ equal(aObject.message, msg, "Expected the original message");
+ } else if (aTopic === "sending-message") {
+ equal(aObject.message, prefix + msg, "Expected the prefixed message.");
+ } else if (aTopic === "new-text") {
+ ok(aObject.QueryInterface(Ci.prplIMessage), "Wrong message type.");
+ ok(prepared, "The message was not prepared before sending.");
+ equal(aObject.message, prefix + msg, "Expected the prefixed message.");
+ receivedMsg = true;
+ aObject.displayMessage = aObject.originalMessage;
+ conv.prepareForDisplaying(aObject);
+ equal(aObject.displayMessage, msg, "Expected the original message");
+ }
+ },
+ });
+
+ conv.sendMsg(msg);
+ ok(receivedMsg, "The new-text notification was never fired.");
+});
+
+// A test that ensures protocols can split messages before they are sent.
+add_task(function test_split_message_before_sending() {
+ let msgCount = 0;
+ let prepared = false;
+
+ let msg = "This is a looo\nooong message.\nThis one is short.";
+ let msgs = msg.split("\n");
+
+ let conv = new Conversation();
+ conv.dispatchMessage = function (aMsg) {
+ equal(aMsg, msgs[msgCount++], "Sending an unexpected message.");
+ };
+ conv.prepareForSending = function (aMsg) {
+ ok(aMsg.QueryInterface(Ci.imIOutgoingMessage), "Wrong message type.");
+ prepared = true;
+ return aMsg.message.split("\n");
+ };
+
+ conv.sendMsg(msg);
+
+ ok(prepared, "Message wasn't prepared for sending.");
+ equal(msgCount, 3, "Not enough messages were sent.");
+});
+
+add_task(function test_removeMessage() {
+ let didRemove = false;
+ let conv = new Conversation();
+ conv.addObserver({
+ observe(subject, topic, data) {
+ if (topic === "remove-text") {
+ equal(data, "foo");
+ didRemove = true;
+ }
+ },
+ });
+
+ conv.removeMessage("foo");
+ ok(didRemove);
+});
diff --git a/comm/chat/modules/test/test_otrlib.js b/comm/chat/modules/test/test_otrlib.js
new file mode 100644
index 0000000000..4b321359f9
--- /dev/null
+++ b/comm/chat/modules/test/test_otrlib.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+/**
+ * Test for libotr.
+ */
+
+"use strict";
+
+const { OTRLibLoader } = ChromeUtils.importESModule(
+ "resource:///modules/OTRLib.sys.mjs"
+);
+
+/**
+ * Initialize libotr.
+ */
+add_setup(async function () {
+ let libOTR = await OTRLibLoader.init();
+ Assert.ok(libOTR.otrl_version, "libotr did load");
+});
diff --git a/comm/chat/modules/test/xpcshell.ini b/comm/chat/modules/test/xpcshell.ini
new file mode 100644
index 0000000000..d12004fd37
--- /dev/null
+++ b/comm/chat/modules/test/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head =
+tail =
+
+[test_filtering.js]
+[test_imThemes.js]
+[test_InteractiveBrowser.js]
+[test_jsProtoHelper.js]
+[test_NormalizedMap.js]
+[test_otrlib.js]