summaryrefslogtreecommitdiffstats
path: root/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/highlighter.js106
-rw-r--r--tests/unit/insertSorted.js76
-rw-r--r--tests/unit/jsParse.js194
-rw-r--r--tests/unit/markup.js143
-rw-r--r--tests/unit/params.js32
-rw-r--r--tests/unit/signalTracker.js115
-rw-r--r--tests/unit/url.js77
-rw-r--r--tests/unit/versionCompare.js52
8 files changed, 795 insertions, 0 deletions
diff --git a/tests/unit/highlighter.js b/tests/unit/highlighter.js
new file mode 100644
index 0000000..d582d38
--- /dev/null
+++ b/tests/unit/highlighter.js
@@ -0,0 +1,106 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for SearchResult description match highlighter
+
+const JsUnit = imports.jsUnit;
+const Pango = imports.gi.Pango;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Util = imports.misc.util;
+
+const tests = [
+ { input: 'abc cba',
+ terms: null,
+ output: 'abc cba' },
+ { input: 'abc cba',
+ terms: [],
+ output: 'abc cba' },
+ { input: 'abc cba',
+ terms: [''],
+ output: 'abc cba' },
+ { input: 'abc cba',
+ terms: ['a'],
+ output: '<b>a</b>bc cb<b>a</b>' },
+ { input: 'abc cba',
+ terms: ['a', 'a'],
+ output: '<b>a</b>bc cb<b>a</b>' },
+ { input: 'CaSe InSenSiTiVe',
+ terms: ['cas', 'sens'],
+ output: '<b>CaS</b>e In<b>SenS</b>iTiVe' },
+ { input: 'This contains the < character',
+ terms: null,
+ output: 'This contains the &lt; character' },
+ { input: 'Don\'t',
+ terms: ['t'],
+ output: 'Don&apos;<b>t</b>' },
+ { input: 'Don\'t',
+ terms: ['n\'t'],
+ output: 'Do<b>n&apos;t</b>' },
+ { input: 'Don\'t',
+ terms: ['o', 't'],
+ output: 'D<b>o</b>n&apos;<b>t</b>' },
+ { input: 'salt&pepper',
+ terms: ['salt'],
+ output: '<b>salt</b>&amp;pepper' },
+ { input: 'salt&pepper',
+ terms: ['salt', 'alt'],
+ output: '<b>salt</b>&amp;pepper' },
+ { input: 'salt&pepper',
+ terms: ['pepper'],
+ output: 'salt&amp;<b>pepper</b>' },
+ { input: 'salt&pepper',
+ terms: ['salt', 'pepper'],
+ output: '<b>salt</b>&amp;<b>pepper</b>' },
+ { input: 'salt&pepper',
+ terms: ['t', 'p'],
+ output: 'sal<b>t</b>&amp;<b>p</b>e<b>p</b><b>p</b>er' },
+ { input: 'salt&pepper',
+ terms: ['t', '&', 'p'],
+ output: 'sal<b>t</b><b>&amp;</b><b>p</b>e<b>p</b><b>p</b>er' },
+ { input: 'salt&pepper',
+ terms: ['e'],
+ output: 'salt&amp;p<b>e</b>pp<b>e</b>r' },
+ { input: 'salt&pepper',
+ terms: ['&a', '&am', '&amp', '&amp;'],
+ output: 'salt&amp;pepper' },
+ { input: '&&&&&',
+ terms: ['a'],
+ output: '&amp;&amp;&amp;&amp;&amp;' },
+ { input: '&;&;&;&;&;',
+ terms: ['a'],
+ output: '&amp;;&amp;;&amp;;&amp;;&amp;;' },
+ { input: '&;&;&;&;&;',
+ terms: [';'],
+ output: '&amp;<b>;</b>&amp;<b>;</b>&amp;<b>;</b>&amp;<b>;</b>&amp;<b>;</b>' },
+ { input: '&amp;',
+ terms: ['a'],
+ output: '&amp;<b>a</b>mp;' }
+];
+
+try {
+ for (let i = 0; i < tests.length; i++) {
+ let highlighter = new Util.Highlighter(tests[i].terms);
+ let output = highlighter.highlight(tests[i].input);
+
+ JsUnit.assertEquals(`Test ${i + 1} highlight ` +
+ `"${tests[i].terms}" in "${tests[i].input}"`,
+ output, tests[i].output);
+
+ let parsed = false;
+ try {
+ Pango.parse_markup(output, -1, '');
+ parsed = true;
+ } catch (e) {}
+ JsUnit.assertEquals(`Test ${i + 1} is valid markup`, true, parsed);
+ }
+} catch (e) {
+ if (typeof(e.isJsUnitException) != 'undefined'
+ && e.isJsUnitException)
+ {
+ if (e.comment)
+ log(`Error in: ${e.comment}`);
+ }
+ throw e;
+}
diff --git a/tests/unit/insertSorted.js b/tests/unit/insertSorted.js
new file mode 100644
index 0000000..610aeed
--- /dev/null
+++ b/tests/unit/insertSorted.js
@@ -0,0 +1,76 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+// Test cases for Util.insertSorted
+
+const JsUnit = imports.jsUnit;
+
+// Needed so that Util can bring some UI stuff
+// we don't actually use
+const Environment = imports.ui.environment;
+Environment.init();
+const Util = imports.misc.util;
+
+function assertArrayEquals(errorMessage, array1, array2) {
+ JsUnit.assertEquals(errorMessage + ' length',
+ array1.length, array2.length);
+ for (let j = 0; j < array1.length; j++) {
+ JsUnit.assertEquals(errorMessage + ' item ' + j,
+ array1[j], array2[j]);
+ }
+}
+
+function cmp(one, two) {
+ return one-two;
+}
+
+let arrayInt = [1, 2, 3, 5, 6];
+Util.insertSorted(arrayInt, 4, cmp);
+
+assertArrayEquals('first test', [1,2,3,4,5,6], arrayInt);
+
+// no comparator, integer sorting is implied
+Util.insertSorted(arrayInt, 3);
+
+assertArrayEquals('second test', [1,2,3,3,4,5,6], arrayInt);
+
+let obj1 = { a: 1 };
+let obj2 = { a: 2, b: 0 };
+let obj3 = { a: 2, b: 1 };
+let obj4 = { a: 3 };
+
+function objCmp(one, two) {
+ return one.a - two.a;
+}
+
+let arrayObj = [obj1, obj3, obj4];
+
+// obj2 compares equivalent to obj3, should be
+// inserted before
+Util.insertSorted(arrayObj, obj2, objCmp);
+
+assertArrayEquals('object test', [obj1, obj2, obj3, obj4], arrayObj);
+
+function checkedCmp(one, two) {
+ if (typeof one != 'number' ||
+ typeof two != 'number')
+ throw new TypeError('Invalid type passed to checkedCmp');
+
+ return one-two;
+}
+
+let arrayEmpty = [];
+
+// check that no comparisons are made when
+// inserting in a empty array
+Util.insertSorted(arrayEmpty, 3, checkedCmp);
+
+// Insert at the end and check that we don't
+// access past it
+Util.insertSorted(arrayEmpty, 4, checkedCmp);
+Util.insertSorted(arrayEmpty, 5, checkedCmp);
+
+// Some more insertions
+Util.insertSorted(arrayEmpty, 2, checkedCmp);
+Util.insertSorted(arrayEmpty, 1, checkedCmp);
+
+assertArrayEquals('checkedCmp test', [1, 2, 3, 4, 5], arrayEmpty);
diff --git a/tests/unit/jsParse.js b/tests/unit/jsParse.js
new file mode 100644
index 0000000..468138b
--- /dev/null
+++ b/tests/unit/jsParse.js
@@ -0,0 +1,194 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+// Test cases for MessageTray URLification
+
+const JsUnit = imports.jsUnit;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const JsParse = imports.misc.jsParse;
+
+const HARNESS_COMMAND_HEADER = "let imports = obj;" +
+ "let global = obj;" +
+ "let Main = obj;" +
+ "let foo = obj;" +
+ "let r = obj;";
+
+const testsFindMatchingQuote = [
+ { input: '"double quotes"',
+ output: 0 },
+ { input: '\'single quotes\'',
+ output: 0 },
+ { input: 'some unquoted "some quoted"',
+ output: 14 },
+ { input: '"mixed \' quotes\'"',
+ output: 0 },
+ { input: '"escaped \\" quote"',
+ output: 0 }
+];
+const testsFindMatchingSlash = [
+ { input: '/slash/',
+ output: 0 },
+ { input: '/slash " with $ funny ^\' stuff/',
+ output: 0 },
+ { input: 'some unslashed /some slashed/',
+ output: 15 },
+ { input: '/escaped \\/ slash/',
+ output: 0 }
+];
+const testsFindMatchingBrace = [
+ { input: '[square brace]',
+ output: 0 },
+ { input: '(round brace)',
+ output: 0 },
+ { input: '([()][nesting!])',
+ output: 0 },
+ { input: '[we have "quoted [" braces]',
+ output: 0 },
+ { input: '[we have /regex [/ braces]',
+ output: 0 },
+ { input: '([[])[] mismatched braces ]',
+ output: 1 }
+];
+const testsGetExpressionOffset = [
+ { input: 'abc.123',
+ output: 0 },
+ { input: 'foo().bar',
+ output: 0 },
+ { input: 'foo(bar',
+ output: 4 },
+ { input: 'foo[abc.match(/"/)]',
+ output: 0 }
+];
+const testsGetDeclaredConstants = [
+ { input: 'const foo = X; const bar = Y;',
+ output: ['foo', 'bar'] },
+ { input: 'const foo=X; const bar=Y',
+ output: ['foo', 'bar'] }
+];
+const testsIsUnsafeExpression = [
+ { input: 'foo.bar',
+ output: false },
+ { input: 'foo[\'bar\']',
+ output: false },
+ { input: 'foo["a=b=c".match(/=/)',
+ output: false },
+ { input: 'foo[1==2]',
+ output: false },
+ { input: '(x=4)',
+ output: true },
+ { input: '(x = 4)',
+ output: true },
+ { input: '(x;y)',
+ output: true }
+];
+const testsModifyScope = [
+ "foo['a",
+ "foo()['b'",
+ "obj.foo()('a', 1, 2, 'b')().",
+ "foo.[.",
+ "foo]]]()))].",
+ "123'ab\"",
+ "Main.foo.bar = 3; bar.",
+ "(Main.foo = 3).",
+ "Main[Main.foo+=-1]."
+];
+
+
+
+// Utility function for comparing arrays
+function assertArrayEquals(errorMessage, array1, array2) {
+ JsUnit.assertEquals(errorMessage + ' length',
+ array1.length, array2.length);
+ for (let j = 0; j < array1.length; j++) {
+ JsUnit.assertEquals(errorMessage + ' item ' + j,
+ array1[j], array2[j]);
+ }
+}
+
+//
+// Test javascript parsing
+//
+
+for (let i = 0; i < testsFindMatchingQuote.length; i++) {
+ let text = testsFindMatchingQuote[i].input;
+ let match = JsParse.findMatchingQuote(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsFindMatchingQuote ' + i,
+ match, testsFindMatchingQuote[i].output);
+}
+
+for (let i = 0; i < testsFindMatchingSlash.length; i++) {
+ let text = testsFindMatchingSlash[i].input;
+ let match = JsParse.findMatchingSlash(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsFindMatchingSlash ' + i,
+ match, testsFindMatchingSlash[i].output);
+}
+
+for (let i = 0; i < testsFindMatchingBrace.length; i++) {
+ let text = testsFindMatchingBrace[i].input;
+ let match = JsParse.findMatchingBrace(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsFindMatchingBrace ' + i,
+ match, testsFindMatchingBrace[i].output);
+}
+
+for (let i = 0; i < testsGetExpressionOffset.length; i++) {
+ let text = testsGetExpressionOffset[i].input;
+ let match = JsParse.getExpressionOffset(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsGetExpressionOffset ' + i,
+ match, testsGetExpressionOffset[i].output);
+}
+
+for (let i = 0; i < testsGetDeclaredConstants.length; i++) {
+ let text = testsGetDeclaredConstants[i].input;
+ let match = JsParse.getDeclaredConstants(text);
+
+ assertArrayEquals('Test testsGetDeclaredConstants ' + i,
+ match, testsGetDeclaredConstants[i].output);
+}
+
+for (let i = 0; i < testsIsUnsafeExpression.length; i++) {
+ let text = testsIsUnsafeExpression[i].input;
+ let unsafe = JsParse.isUnsafeExpression(text);
+
+ JsUnit.assertEquals('Test testsIsUnsafeExpression ' + i,
+ unsafe, testsIsUnsafeExpression[i].output);
+}
+
+//
+// Test safety of eval to get completions
+//
+
+for (let i = 0; i < testsModifyScope.length; i++) {
+ let text = testsModifyScope[i];
+ // We need to use var here for the with statement
+ var obj = {};
+
+ // Just as in JsParse.getCompletions, we will find the offset
+ // of the expression, test whether it is unsafe, and then eval it.
+ let offset = JsParse.getExpressionOffset(text, text.length - 1);
+ if (offset >= 0) {
+ text = text.slice(offset);
+
+ let matches = text.match(/(.*)\.(.*)/);
+ if (matches) {
+ let [expr, base, attrHead] = matches;
+
+ if (!JsParse.isUnsafeExpression(base)) {
+ with (obj) {
+ try {
+ eval(HARNESS_COMMAND_HEADER + base);
+ } catch (e) {
+ JsUnit.assertNotEquals("Code '" + base + "' is valid code", e.constructor, SyntaxError);
+ }
+ }
+ }
+ }
+ }
+ let propertyNames = Object.getOwnPropertyNames(obj);
+ JsUnit.assertEquals("The context '" + JSON.stringify(obj) + "' was not modified", propertyNames.length, 0);
+}
diff --git a/tests/unit/markup.js b/tests/unit/markup.js
new file mode 100644
index 0000000..603ca81
--- /dev/null
+++ b/tests/unit/markup.js
@@ -0,0 +1,143 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for MessageList markup parsing
+
+const JsUnit = imports.jsUnit;
+const Pango = imports.gi.Pango;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Main = imports.ui.main; // unused, but needed to break dependency loop
+const MessageList = imports.ui.messageList;
+
+// Assert that @input, assumed to be markup, gets "fixed" to @output,
+// which is valid markup. If @output is null, @input is expected to
+// convert to itself
+function assertConverts(input, output) {
+ if (!output)
+ output = input;
+ let fixed = MessageList._fixMarkup(input, true);
+ JsUnit.assertEquals(output, fixed);
+
+ let parsed = false;
+ try {
+ Pango.parse_markup(fixed, -1, '');
+ parsed = true;
+ } catch (e) {}
+ JsUnit.assertEquals(true, parsed);
+}
+
+// Assert that @input, assumed to be plain text, gets escaped to @output,
+// which is valid markup.
+function assertEscapes(input, output) {
+ let fixed = MessageList._fixMarkup(input, false);
+ JsUnit.assertEquals(output, fixed);
+
+ let parsed = false;
+ try {
+ Pango.parse_markup(fixed, -1, '');
+ parsed = true;
+ } catch (e) {}
+ JsUnit.assertEquals(true, parsed);
+}
+
+
+
+// CORRECT MARKUP
+
+assertConverts('foo');
+assertEscapes('foo', 'foo');
+
+assertConverts('<b>foo</b>');
+assertEscapes('<b>foo</b>', '&lt;b&gt;foo&lt;/b&gt;');
+
+assertConverts('something <i>foo</i>');
+assertEscapes('something <i>foo</i>', 'something &lt;i&gt;foo&lt;/i&gt;');
+
+assertConverts('<u>foo</u> something');
+assertEscapes('<u>foo</u> something', '&lt;u&gt;foo&lt;/u&gt; something');
+
+assertConverts('<b>bold</b> <i>italic <u>and underlined</u></i>');
+assertEscapes('<b>bold</b> <i>italic <u>and underlined</u></i>', '&lt;b&gt;bold&lt;/b&gt; &lt;i&gt;italic &lt;u&gt;and underlined&lt;/u&gt;&lt;/i&gt;');
+
+assertConverts('this &amp; that');
+assertEscapes('this &amp; that', 'this &amp;amp; that');
+
+assertConverts('this &lt; that');
+assertEscapes('this &lt; that', 'this &amp;lt; that');
+
+assertConverts('this &lt; that &gt; the other');
+assertEscapes('this &lt; that &gt; the other', 'this &amp;lt; that &amp;gt; the other');
+
+assertConverts('this &lt;<i>that</i>&gt;');
+assertEscapes('this &lt;<i>that</i>&gt;', 'this &amp;lt;&lt;i&gt;that&lt;/i&gt;&amp;gt;');
+
+assertConverts('<b>this</b> > <i>that</i>');
+assertEscapes('<b>this</b> > <i>that</i>', '&lt;b&gt;this&lt;/b&gt; &gt; &lt;i&gt;that&lt;/i&gt;');
+
+
+
+// PARTIALLY CORRECT MARKUP
+// correct bits are kept, incorrect bits are escaped
+
+// unrecognized entity
+assertConverts('<b>smile</b> &#9786;!', '<b>smile</b> &amp;#9786;!');
+assertEscapes('<b>smile</b> &#9786;!', '&lt;b&gt;smile&lt;/b&gt; &amp;#9786;!');
+
+// stray '&'; this is really a bug, but it's easier to do it this way
+assertConverts('<b>this</b> & <i>that</i>', '<b>this</b> &amp; <i>that</i>');
+assertEscapes('<b>this</b> & <i>that</i>', '&lt;b&gt;this&lt;/b&gt; &amp; &lt;i&gt;that&lt;/i&gt;');
+
+// likewise with stray '<'
+assertConverts('this < that', 'this &lt; that');
+assertEscapes('this < that', 'this &lt; that');
+
+assertConverts('<b>this</b> < <i>that</i>', '<b>this</b> &lt; <i>that</i>');
+assertEscapes('<b>this</b> < <i>that</i>', '&lt;b&gt;this&lt;/b&gt; &lt; &lt;i&gt;that&lt;/i&gt;');
+
+assertConverts('this < that > the other', 'this &lt; that > the other');
+assertEscapes('this < that > the other', 'this &lt; that &gt; the other');
+
+assertConverts('this <<i>that</i>>', 'this &lt;<i>that</i>>');
+assertEscapes('this <<i>that</i>>', 'this &lt;&lt;i&gt;that&lt;/i&gt;&gt;');
+
+// unknown tags
+assertConverts('<unknown>tag</unknown>', '&lt;unknown>tag&lt;/unknown>');
+assertEscapes('<unknown>tag</unknown>', '&lt;unknown&gt;tag&lt;/unknown&gt;');
+
+// make sure we check beyond the first letter
+assertConverts('<bunknown>tag</bunknown>', '&lt;bunknown>tag&lt;/bunknown>');
+assertEscapes('<bunknown>tag</bunknown>', '&lt;bunknown&gt;tag&lt;/bunknown&gt;');
+
+// with mix of good and bad, we keep the good and escape the bad
+assertConverts('<i>known</i> and <unknown>tag</unknown>', '<i>known</i> and &lt;unknown>tag&lt;/unknown>');
+assertEscapes('<i>known</i> and <unknown>tag</unknown>', '&lt;i&gt;known&lt;/i&gt; and &lt;unknown&gt;tag&lt;/unknown&gt;');
+
+
+
+// FULLY INCORRECT MARKUP
+// (fall back to escaping the whole thing)
+
+// tags not matched up
+assertConverts('<b>in<i>com</i>plete', '&lt;b&gt;in&lt;i&gt;com&lt;/i&gt;plete');
+assertEscapes('<b>in<i>com</i>plete', '&lt;b&gt;in&lt;i&gt;com&lt;/i&gt;plete');
+
+assertConverts('in<i>com</i>plete</b>', 'in&lt;i&gt;com&lt;/i&gt;plete&lt;/b&gt;');
+assertEscapes('in<i>com</i>plete</b>', 'in&lt;i&gt;com&lt;/i&gt;plete&lt;/b&gt;');
+
+// we don't support attributes, and it's too complicated to try
+// to escape both start and end tags, so we just treat it as bad
+assertConverts('<b>good</b> and <b style=\'bad\'>bad</b>', '&lt;b&gt;good&lt;/b&gt; and &lt;b style=&apos;bad&apos;&gt;bad&lt;/b&gt;');
+assertEscapes('<b>good</b> and <b style=\'bad\'>bad</b>', '&lt;b&gt;good&lt;/b&gt; and &lt;b style=&apos;bad&apos;&gt;bad&lt;/b&gt;');
+
+// this is just syntactically invalid
+assertConverts('<b>unrecognized</b stuff>', '&lt;b&gt;unrecognized&lt;/b stuff&gt;');
+assertEscapes('<b>unrecognized</b stuff>', '&lt;b&gt;unrecognized&lt;/b stuff&gt;');
+
+// mismatched tags
+assertConverts('<b>mismatched</i>', '&lt;b&gt;mismatched&lt;/i&gt;');
+assertEscapes('<b>mismatched</i>', '&lt;b&gt;mismatched&lt;/i&gt;');
+
+assertConverts('<b>mismatched/unknown</bunknown>', '&lt;b&gt;mismatched/unknown&lt;/bunknown&gt;');
+assertEscapes('<b>mismatched/unknown</bunknown>', '&lt;b&gt;mismatched/unknown&lt;/bunknown&gt;');
diff --git a/tests/unit/params.js b/tests/unit/params.js
new file mode 100644
index 0000000..6ac4cc1
--- /dev/null
+++ b/tests/unit/params.js
@@ -0,0 +1,32 @@
+const JsUnit = imports.jsUnit;
+const Params = imports.misc.params;
+
+function assertParamsEqual(params, expected) {
+ for (let p in params) {
+ JsUnit.assertTrue(p in expected);
+ JsUnit.assertEquals(params[p], expected[p]);
+ }
+}
+
+let defaults = {
+ foo: 'This is a test',
+ bar: null,
+ baz: 42
+};
+
+assertParamsEqual(
+ Params.parse(null, defaults),
+ defaults);
+
+assertParamsEqual(
+ Params.parse({ bar: 23 }, defaults),
+ { foo: 'This is a test', bar: 23, baz: 42 });
+
+JsUnit.assertRaises(
+ () => {
+ Params.parse({ extraArg: 'quz' }, defaults);
+ });
+
+assertParamsEqual(
+ Params.parse({ extraArg: 'quz' }, defaults, true),
+ { foo: 'This is a test', bar: null, baz: 42, extraArg: 'quz' });
diff --git a/tests/unit/signalTracker.js b/tests/unit/signalTracker.js
new file mode 100644
index 0000000..7943d0a
--- /dev/null
+++ b/tests/unit/signalTracker.js
@@ -0,0 +1,115 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for version comparison
+
+const { GObject } = imports.gi;
+
+const JsUnit = imports.jsUnit;
+const Signals = imports.misc.signals;
+
+const Environment = imports.ui.environment;
+const { TransientSignalHolder, registerDestroyableType } = imports.misc.signalTracker;
+
+Environment.init();
+
+const Destroyable = GObject.registerClass({
+ Signals: { 'destroy': {} },
+}, class Destroyable extends GObject.Object {});
+registerDestroyableType(Destroyable);
+
+const GObjectEmitter = GObject.registerClass({
+ Signals: { 'signal': {} },
+}, class GObjectEmitter extends Destroyable {});
+
+const emitter1 = new Signals.EventEmitter();
+const emitter2 = new GObjectEmitter();
+
+const tracked1 = new Destroyable();
+const tracked2 = {};
+
+let count = 0;
+const handler = () => count++;
+
+emitter1.connectObject('signal', handler, tracked1);
+emitter2.connectObject('signal', handler, tracked1);
+
+emitter1.connectObject('signal', handler, tracked2);
+emitter2.connectObject('signal', handler, tracked2);
+
+JsUnit.assertEquals(count, 0);
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 4);
+
+tracked1.emit('destroy');
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 6);
+
+emitter1.disconnectObject(tracked2);
+emitter2.emit('destroy');
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 6);
+
+emitter1.connectObject(
+ 'signal', handler,
+ 'signal', handler, GObject.ConnectFlags.AFTER,
+ tracked1);
+emitter2.connectObject(
+ 'signal', handler,
+ 'signal', handler, GObject.ConnectFlags.AFTER,
+ tracked1);
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 10);
+
+tracked1.emit('destroy');
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 10);
+
+emitter1.connectObject('signal', handler, tracked1);
+emitter2.connectObject('signal', handler, tracked1);
+
+transientHolder = new TransientSignalHolder(tracked1);
+
+emitter1.connectObject('signal', handler, transientHolder);
+emitter2.connectObject('signal', handler, transientHolder);
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 14);
+
+transientHolder.destroy();
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 16);
+
+transientHolder = new TransientSignalHolder(tracked1);
+
+emitter1.connectObject('signal', handler, transientHolder);
+emitter2.connectObject('signal', handler, transientHolder);
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 20);
+
+tracked1.emit('destroy');
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 20);
diff --git a/tests/unit/url.js b/tests/unit/url.js
new file mode 100644
index 0000000..84aecc9
--- /dev/null
+++ b/tests/unit/url.js
@@ -0,0 +1,77 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for MessageTray URLification
+
+const JsUnit = imports.jsUnit;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Util = imports.misc.util;
+
+const tests = [
+ { input: 'This is a test',
+ output: [] },
+ { input: 'This is http://www.gnome.org a test',
+ output: [ { url: 'http://www.gnome.org', pos: 8 } ] },
+ { input: 'This is http://www.gnome.org',
+ output: [ { url: 'http://www.gnome.org', pos: 8 } ] },
+ { input: 'http://www.gnome.org a test',
+ output: [ { url: 'http://www.gnome.org', pos: 0 } ] },
+ { input: 'http://www.gnome.org',
+ output: [ { url: 'http://www.gnome.org', pos: 0 } ] },
+ { input: 'Go to http://www.gnome.org.',
+ output: [ { url: 'http://www.gnome.org', pos: 6 } ] },
+ { input: 'Go to http://www.gnome.org/.',
+ output: [ { url: 'http://www.gnome.org/', pos: 6 } ] },
+ { input: '(Go to http://www.gnome.org!)',
+ output: [ { url: 'http://www.gnome.org', pos: 7 } ] },
+ { input: 'Use GNOME (http://www.gnome.org).',
+ output: [ { url: 'http://www.gnome.org', pos: 11 } ] },
+ { input: 'This is a http://www.gnome.org/path test.',
+ output: [ { url: 'http://www.gnome.org/path', pos: 10 } ] },
+ { input: 'This is a www.gnome.org scheme-less test.',
+ output: [ { url: 'www.gnome.org', pos: 10 } ] },
+ { input: 'This is a www.gnome.org/scheme-less test.',
+ output: [ { url: 'www.gnome.org/scheme-less', pos: 10 } ] },
+ { input: 'This is a http://www.gnome.org:99/port test.',
+ output: [ { url: 'http://www.gnome.org:99/port', pos: 10 } ] },
+ { input: 'This is an ftp://www.gnome.org/ test.',
+ output: [ { url: 'ftp://www.gnome.org/', pos: 11 } ] },
+ { input: 'https://www.gnome.org/(some_url,_with_very_unusual_characters)',
+ output: [ { url: 'https://www.gnome.org/(some_url,_with_very_unusual_characters)', pos: 0 } ] },
+ { input: 'https://www.gnome.org/(some_url_with_unbalanced_parenthesis',
+ output: [ { url: 'https://www.gnome.org/', pos: 0 } ] },
+ { input: 'https://www.gnome.org/‎ plus trailing junk',
+ output: [ { url: 'https://www.gnome.org/', pos: 0 } ] },
+
+ { input: 'Visit http://www.gnome.org/ and http://developer.gnome.org',
+ output: [ { url: 'http://www.gnome.org/', pos: 6 },
+ { url: 'http://developer.gnome.org', pos: 32 } ] },
+
+ { input: 'This is not.a.domain test.',
+ output: [ ] },
+ { input: 'This is not:a.url test.',
+ output: [ ] },
+ { input: 'This is not:/a.url/ test.',
+ output: [ ] },
+ { input: 'This is not:/a.url/ test.',
+ output: [ ] },
+ { input: 'This is not@a.url/ test.',
+ output: [ ] },
+ { input: 'This is surely@not.a/url test.',
+ output: [ ] }
+];
+
+for (let i = 0; i < tests.length; i++) {
+ let match = Util.findUrls(tests[i].input);
+
+ JsUnit.assertEquals('Test ' + i + ' match length',
+ match.length, tests[i].output.length);
+ for (let j = 0; j < match.length; j++) {
+ JsUnit.assertEquals('Test ' + i + ', match ' + j + ' url',
+ match[j].url, tests[i].output[j].url);
+ JsUnit.assertEquals('Test ' + i + ', match ' + j + ' position',
+ match[j].pos, tests[i].output[j].pos);
+ }
+}
diff --git a/tests/unit/versionCompare.js b/tests/unit/versionCompare.js
new file mode 100644
index 0000000..1997a6c
--- /dev/null
+++ b/tests/unit/versionCompare.js
@@ -0,0 +1,52 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for version comparison
+
+const JsUnit = imports.jsUnit;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Util = imports.misc.util;
+
+const tests = [
+ { v1: '40',
+ v2: '40',
+ res: 0 },
+ { v1: '40',
+ v2: '42',
+ res: -1 },
+ { v1: '42',
+ v2: '40',
+ res: 1 },
+ { v1: '3.38.0',
+ v2: '40',
+ res: -1 },
+ { v1: '40',
+ v2: '3.38.0',
+ res: 1 },
+ { v1: '40',
+ v2: '3.38.0',
+ res: 1 },
+ { v1: '40.alpha.1.1',
+ v2: '40',
+ res: -1 },
+ { v1: '40',
+ v2: '40.alpha.1.1',
+ res: 1 },
+ { v1: '40.beta',
+ v2: '40',
+ res: -1 },
+ { v1: '40.1',
+ v2: '40',
+ res: 1 },
+ { v1: '',
+ v2: '40.alpha',
+ res: -1 },
+];
+
+for (let i = 0; i < tests.length; i++) {
+ name = 'Test #' + i + ' v1: ' + tests[i].v1 + ', v2: ' + tests[i].v2;
+ print(name);
+ JsUnit.assertEquals(name, Util.GNOMEversionCompare (tests[i].v1, tests[i].v2), tests[i].res);
+}