mirror of
https://github.com/discourse/discourse.git
synced 2025-05-21 18:12:32 +08:00
Renamed components
to lib
in the JS project, as Ember has components and they mean something different.
This commit is contained in:
@ -1,123 +0,0 @@
|
||||
/*global md5:true */
|
||||
module("Discourse.BBCode");
|
||||
|
||||
var format = function(input, expected, text) {
|
||||
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false});
|
||||
equal(cooked, "<p>" + expected + "</p>", text);
|
||||
};
|
||||
|
||||
test('basic bbcode', function() {
|
||||
format("[b]strong[/b]", "<span class=\"bbcode-b\">strong</span>", "bolds text");
|
||||
format("[i]emphasis[/i]", "<span class=\"bbcode-i\">emphasis</span>", "italics text");
|
||||
format("[u]underlined[/u]", "<span class=\"bbcode-u\">underlined</span>", "underlines text");
|
||||
format("[s]strikethrough[/s]", "<span class=\"bbcode-s\">strikethrough</span>", "strikes-through text");
|
||||
format("[spoiler]it's a sled[/spoiler]", "<span class=\"spoiler\">it's a sled</span>", "supports spoiler tags");
|
||||
format("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\"/>", "links images");
|
||||
format("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without a title");
|
||||
format("[email]eviltrout@mailinator.com[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports [email] without a title");
|
||||
format("[b]evil [i]trout[/i][/b]",
|
||||
"<span class=\"bbcode-b\">evil <span class=\"bbcode-i\">trout</span></span>",
|
||||
"allows embedding of tags");
|
||||
});
|
||||
|
||||
test('invalid bbcode', function() {
|
||||
var cooked = Discourse.Markdown.cook("[code]I am not closed\n\nThis text exists.", {lookupAvatar: false});
|
||||
equal(cooked, "<p>[code]I am not closed</p>\n\n<p>This text exists.</p>", "does not raise an error with an open bbcode tag.");
|
||||
});
|
||||
|
||||
test('code', function() {
|
||||
format("[code]\nx++\n[/code]", "<pre>\nx++</pre>", "makes code into pre");
|
||||
format("[code]\nx++\ny++\nz++\n[/code]", "<pre>\nx++\ny++\nz++</pre>", "makes code into pre");
|
||||
format("[code]abc\n#def\n[/code]", '<pre>abc\n#def</pre>', 'it handles headings in a [code] block');
|
||||
});
|
||||
|
||||
test('lists', function() {
|
||||
format("[ul][li]option one[/li][/ul]", "<ul><li>option one</li></ul>", "creates an ul");
|
||||
format("[ol][li]option one[/li][/ol]", "<ol><li>option one</li></ol>", "creates an ol");
|
||||
});
|
||||
|
||||
test('tags with arguments', function() {
|
||||
format("[url=http://bettercallsaul.com]better call![/url]", "<a href=\"http://bettercallsaul.com\">better call!</a>", "supports [url] with a title");
|
||||
format("[email=eviltrout@mailinator.com]evil trout[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">evil trout</a>", "supports [email] with a title");
|
||||
format("[u][i]abc[/i][/u]", "<span class=\"bbcode-u\"><span class=\"bbcode-i\">abc</span></span>", "can nest tags");
|
||||
format("[b]first[/b] [b]second[/b]", "<span class=\"bbcode-b\">first</span> <span class=\"bbcode-b\">second</span>", "can bold two things on the same line");
|
||||
});
|
||||
|
||||
test("size tags", function() {
|
||||
format("[size=35]BIG [b]whoop[/b][/size]",
|
||||
"<span class=\"bbcode-size-35\">BIG <span class=\"bbcode-b\">whoop</span></span>",
|
||||
"supports [size=]");
|
||||
format("[size=asdf]regular[/size]",
|
||||
"<span class=\"bbcode-size-1\">regular</span>",
|
||||
"it only supports numbers in bbcode");
|
||||
});
|
||||
|
||||
test("quotes", function() {
|
||||
|
||||
var post = Discourse.Post.create({
|
||||
cooked: "<p><b>lorem</b> ipsum</p>",
|
||||
username: "eviltrout",
|
||||
post_number: 1,
|
||||
topic_id: 2
|
||||
});
|
||||
|
||||
var formatQuote = function(val, expected, text) {
|
||||
equal(Discourse.Quote.build(post, val), expected, text);
|
||||
};
|
||||
|
||||
formatQuote(undefined, "", "empty string for undefined content");
|
||||
formatQuote(null, "", "empty string for null content");
|
||||
formatQuote("", "", "empty string for empty string content");
|
||||
|
||||
formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes");
|
||||
|
||||
formatQuote(" lorem \t ",
|
||||
"[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n",
|
||||
"trims white spaces before & after the quoted contents");
|
||||
|
||||
formatQuote("lorem ipsum",
|
||||
"[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n",
|
||||
"marks quotes as full when the quote is the full message");
|
||||
|
||||
formatQuote("**lorem** ipsum",
|
||||
"[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n",
|
||||
"keeps BBCode formatting");
|
||||
|
||||
formatQuote("this is <not> a bug",
|
||||
"[quote=\"eviltrout, post:1, topic:2\"]\nthis is <not> a bug\n[/quote]\n\n",
|
||||
"it escapes the contents of the quote");
|
||||
|
||||
format("[quote]test[/quote]",
|
||||
"<aside class=\"quote\"><blockquote><p>test</p></blockquote></aside>",
|
||||
"it supports quotes without params");
|
||||
|
||||
});
|
||||
|
||||
test("quote formatting", function() {
|
||||
|
||||
format("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]",
|
||||
"<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
|
||||
"<div class=\"quote-controls\"></div>EvilTrout said:</div><blockquote><p>[sam]</p></blockquote></aside>",
|
||||
"it allows quotes with [] inside");
|
||||
|
||||
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
|
||||
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:" +
|
||||
"</div><blockquote><p>abc</p></blockquote></aside>",
|
||||
"renders quotes properly");
|
||||
|
||||
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]\nhello",
|
||||
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:" +
|
||||
"</div><blockquote><p>abc</p></blockquote></aside></p>\n\n<p>hello",
|
||||
"handles new lines properly");
|
||||
|
||||
});
|
||||
|
||||
test("quotes with trailing formatting", function() {
|
||||
var cooked = Discourse.Markdown.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]\nhello\n[/quote]\n*Test*", {lookupAvatar: false});
|
||||
equal(cooked,
|
||||
"<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
|
||||
"<div class=\"quote-controls\"></div>EvilTrout said:</div><blockquote><p>hello</p></blockquote></aside></p>\n\n<p><em>Test</em></p>",
|
||||
"it allows trailing formatting");
|
||||
});
|
||||
|
||||
|
@ -1,179 +0,0 @@
|
||||
module("Discourse.ClickTrack", {
|
||||
setup: function() {
|
||||
|
||||
// Prevent any of these tests from navigating away
|
||||
this.win = {focus: function() { } };
|
||||
this.redirectTo = sinon.stub(Discourse.URL, "redirectTo");
|
||||
sinon.stub(Discourse, "ajax");
|
||||
this.windowOpen = sinon.stub(window, "open").returns(this.win);
|
||||
sinon.stub(this.win, "focus");
|
||||
|
||||
$('#qunit-scratch').html([
|
||||
'<div id="topic" id="1337">',
|
||||
' <article data-post-id="42" data-user-id="3141">',
|
||||
' <a href="http://www.google.com">google.com</a>',
|
||||
' <a class="lightbox back quote-other-topic" href="http://www.google.com">google.com</a>',
|
||||
' <a id="with-badge" data-user-id="314" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||
' <a id="with-badge-but-not-mine" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||
' <div class="onebox-result">',
|
||||
' <a id="inside-onebox" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||
' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||
' </div>',
|
||||
' <a id="same-site" href="http://discuss.domain.com">forum</a>',
|
||||
' <a class="attachment" href="http://discuss.domain.com/uploads/default/1234/1532357280.txt">log.txt</a>',
|
||||
' </article>',
|
||||
'</div>'].join("\n"));
|
||||
},
|
||||
|
||||
teardown: function() {
|
||||
$('#topic').remove();
|
||||
$('#qunit-scratch').html('');
|
||||
|
||||
Discourse.URL.redirectTo.restore();
|
||||
Discourse.ajax.restore();
|
||||
window.open.restore();
|
||||
this.win.focus.restore();
|
||||
}
|
||||
});
|
||||
|
||||
var track = Discourse.ClickTrack.trackClick;
|
||||
|
||||
// test
|
||||
var generateClickEventOn = function(selector) {
|
||||
return $.Event("click", { currentTarget: $(selector)[0] });
|
||||
};
|
||||
|
||||
test("does not track clicks on lightboxes", function() {
|
||||
var clickEvent = generateClickEventOn('.lightbox');
|
||||
this.stub(clickEvent, "preventDefault");
|
||||
ok(track(clickEvent));
|
||||
ok(!clickEvent.preventDefault.calledOnce);
|
||||
});
|
||||
|
||||
test("it calls preventDefault when clicking on an a", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
this.stub(clickEvent, "preventDefault");
|
||||
track(clickEvent);
|
||||
ok(clickEvent.preventDefault.calledOnce);
|
||||
ok(Discourse.URL.redirectTo.calledOnce);
|
||||
});
|
||||
|
||||
test("does not track clicks on back buttons", function() {
|
||||
ok(track(generateClickEventOn('.back')));
|
||||
});
|
||||
|
||||
test("does not track clicks on quote buttons", function() {
|
||||
ok(track(generateClickEventOn('.quote-other-topic')));
|
||||
});
|
||||
|
||||
test("removes the href and put it as a data attribute", function() {
|
||||
track(generateClickEventOn('a'));
|
||||
|
||||
var $link = $('a').first();
|
||||
ok($link.hasClass('no-href'));
|
||||
equal($link.data('href'), 'http://www.google.com');
|
||||
blank($link.attr('href'));
|
||||
ok($link.data('auto-route'));
|
||||
ok(Discourse.URL.redirectTo.calledOnce);
|
||||
});
|
||||
|
||||
|
||||
var badgeClickCount = function(id, expected) {
|
||||
track(generateClickEventOn('#' + id));
|
||||
var $badge = $('span.badge', $('#' + id).first());
|
||||
equal(parseInt($badge.html(), 10), expected);
|
||||
};
|
||||
|
||||
test("does not update badge clicks on my own link", function() {
|
||||
this.stub(Discourse.User, 'currentProp').withArgs('id').returns(314);
|
||||
badgeClickCount('with-badge', 1);
|
||||
});
|
||||
|
||||
test("does not update badge clicks in my own post", function() {
|
||||
this.stub(Discourse.User, 'currentProp').withArgs('id').returns(3141);
|
||||
badgeClickCount('with-badge-but-not-mine', 1);
|
||||
});
|
||||
|
||||
test("updates badge counts correctly", function() {
|
||||
badgeClickCount('inside-onebox', 1);
|
||||
badgeClickCount('inside-onebox-forced', 2);
|
||||
badgeClickCount('with-badge', 2);
|
||||
});
|
||||
|
||||
var trackRightClick = function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
clickEvent.which = 3;
|
||||
return track(clickEvent);
|
||||
};
|
||||
|
||||
test("right clicks change the href", function() {
|
||||
ok(trackRightClick());
|
||||
equal($('a').first().prop('href'), "http://www.google.com/");
|
||||
});
|
||||
|
||||
test("right clicks are tracked", function() {
|
||||
Discourse.SiteSettings.track_external_right_clicks = true;
|
||||
trackRightClick();
|
||||
equal($('a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42");
|
||||
});
|
||||
|
||||
|
||||
var expectToOpenInANewTab = function(clickEvent) {
|
||||
ok(!track(clickEvent));
|
||||
ok(Discourse.ajax.calledOnce);
|
||||
ok(window.open.calledOnce);
|
||||
};
|
||||
|
||||
test("it opens in a new tab when pressing shift", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
clickEvent.shiftKey = true;
|
||||
expectToOpenInANewTab(clickEvent);
|
||||
});
|
||||
|
||||
test("it opens in a new tab when pressing meta", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
clickEvent.metaKey = true;
|
||||
expectToOpenInANewTab(clickEvent);
|
||||
});
|
||||
|
||||
test("it opens in a new tab when pressing meta", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
clickEvent.ctrlKey = true;
|
||||
expectToOpenInANewTab(clickEvent);
|
||||
});
|
||||
|
||||
test("it opens in a new tab when pressing meta", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
clickEvent.which = 2;
|
||||
expectToOpenInANewTab(clickEvent);
|
||||
});
|
||||
|
||||
test("tracks via AJAX if we're on the same site", function() {
|
||||
this.stub(Discourse.URL, "routeTo");
|
||||
this.stub(Discourse.URL, "origin").returns("http://discuss.domain.com");
|
||||
|
||||
ok(!track(generateClickEventOn('#same-site')));
|
||||
ok(Discourse.ajax.calledOnce);
|
||||
ok(Discourse.URL.routeTo.calledOnce);
|
||||
});
|
||||
|
||||
test("does not track via AJAX for attachments", function() {
|
||||
this.stub(Discourse.URL, "routeTo");
|
||||
this.stub(Discourse.URL, "origin").returns("http://discuss.domain.com");
|
||||
|
||||
ok(!track(generateClickEventOn('.attachment')));
|
||||
ok(Discourse.URL.redirectTo.calledOnce);
|
||||
});
|
||||
|
||||
test("tracks custom urls when opening in another window", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
this.stub(Discourse.User, "currentProp").withArgs('external_links_in_new_tab').returns(true);
|
||||
ok(!track(clickEvent));
|
||||
ok(this.windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42', '_blank'));
|
||||
});
|
||||
|
||||
test("tracks custom urls when opening in another window", function() {
|
||||
var clickEvent = generateClickEventOn('a');
|
||||
ok(!track(clickEvent));
|
||||
ok(this.redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42'));
|
||||
});
|
@ -1,91 +0,0 @@
|
||||
module("Discourse.Computed", {
|
||||
setup: function() {
|
||||
sinon.stub(I18n, "t", function(scope) {
|
||||
return "%@ translated: " + scope;
|
||||
});
|
||||
},
|
||||
|
||||
teardown: function() {
|
||||
I18n.t.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("propertyEqual", function() {
|
||||
var t = Em.Object.extend({
|
||||
same: Discourse.computed.propertyEqual('cookies', 'biscuits')
|
||||
}).create({
|
||||
cookies: 10,
|
||||
biscuits: 10
|
||||
});
|
||||
|
||||
ok(t.get('same'), "it is true when the properties are the same");
|
||||
t.set('biscuits', 9);
|
||||
ok(!t.get('same'), "it isn't true when one property is different");
|
||||
});
|
||||
|
||||
test("propertyNotEqual", function() {
|
||||
var t = Em.Object.extend({
|
||||
diff: Discourse.computed.propertyNotEqual('cookies', 'biscuits')
|
||||
}).create({
|
||||
cookies: 10,
|
||||
biscuits: 10
|
||||
});
|
||||
|
||||
ok(!t.get('diff'), "it isn't true when the properties are the same");
|
||||
t.set('biscuits', 9);
|
||||
ok(t.get('diff'), "it is true when one property is different");
|
||||
});
|
||||
|
||||
|
||||
test("fmt", function() {
|
||||
var t = Em.Object.extend({
|
||||
exclaimyUsername: Discourse.computed.fmt('username', "!!! %@ !!!"),
|
||||
multiple: Discourse.computed.fmt('username', 'mood', "%@ is %@")
|
||||
}).create({
|
||||
username: 'eviltrout',
|
||||
mood: "happy"
|
||||
});
|
||||
|
||||
equal(t.get('exclaimyUsername'), '!!! eviltrout !!!', "it inserts the string");
|
||||
equal(t.get('multiple'), "eviltrout is happy", "it inserts multiple strings");
|
||||
|
||||
t.set('username', 'codinghorror');
|
||||
equal(t.get('multiple'), "codinghorror is happy", "it supports changing properties");
|
||||
t.set('mood', 'ecstatic');
|
||||
equal(t.get('multiple'), "codinghorror is ecstatic", "it supports changing another property");
|
||||
});
|
||||
|
||||
|
||||
test("i18n", function() {
|
||||
var t = Em.Object.extend({
|
||||
exclaimyUsername: Discourse.computed.i18n('username', "!!! %@ !!!"),
|
||||
multiple: Discourse.computed.i18n('username', 'mood', "%@ is %@")
|
||||
}).create({
|
||||
username: 'eviltrout',
|
||||
mood: "happy"
|
||||
});
|
||||
|
||||
equal(t.get('exclaimyUsername'), '%@ translated: !!! eviltrout !!!', "it inserts the string and then translates");
|
||||
equal(t.get('multiple'), "%@ translated: eviltrout is happy", "it inserts multiple strings and then translates");
|
||||
|
||||
t.set('username', 'codinghorror');
|
||||
equal(t.get('multiple'), "%@ translated: codinghorror is happy", "it supports changing properties");
|
||||
t.set('mood', 'ecstatic');
|
||||
equal(t.get('multiple'), "%@ translated: codinghorror is ecstatic", "it supports changing another property");
|
||||
});
|
||||
|
||||
|
||||
test("url", function() {
|
||||
var t, testClass;
|
||||
|
||||
testClass = Em.Object.extend({
|
||||
userUrl: Discourse.computed.url('username', "/users/%@")
|
||||
});
|
||||
|
||||
t = testClass.create({ username: 'eviltrout' });
|
||||
equal(t.get('userUrl'), "/users/eviltrout", "it supports urls without a prefix");
|
||||
|
||||
Discourse.BaseUri = "/prefixed/";
|
||||
t = testClass.create({ username: 'eviltrout' });
|
||||
equal(t.get('userUrl'), "/prefixed/users/eviltrout", "it supports urls with a prefix");
|
||||
});
|
@ -1,94 +0,0 @@
|
||||
var clock, original, debounced, originalPromiseResolvesWith, callback;
|
||||
|
||||
var nothingFired = function(additionalMessage) {
|
||||
ok(!original.called, "original function is not called " + additionalMessage);
|
||||
ok(!callback.called, "debounced promise is not resolved " + additionalMessage);
|
||||
};
|
||||
|
||||
var originalAndCallbackFiredOnce = function(additionalMessage) {
|
||||
ok(original.calledOnce, "original function is called once " + additionalMessage);
|
||||
ok(callback.calledOnce, "debounced promise is resolved once " + additionalMessage);
|
||||
};
|
||||
|
||||
module("Discourse.debouncePromise", {
|
||||
setup: function() {
|
||||
clock = sinon.useFakeTimers();
|
||||
|
||||
originalPromiseResolvesWith = null;
|
||||
original = sinon.spy(function() {
|
||||
var promise = Ember.Deferred.create();
|
||||
promise.resolve(originalPromiseResolvesWith);
|
||||
return promise;
|
||||
});
|
||||
|
||||
debounced = Discourse.debouncePromise(original, 100);
|
||||
callback = sinon.spy();
|
||||
},
|
||||
|
||||
teardown: function() {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("delays execution till the end of the timeout", function() {
|
||||
debounced().then(callback);
|
||||
nothingFired("immediately after calling debounced function");
|
||||
|
||||
clock.tick(99);
|
||||
nothingFired("just before the end of the timeout");
|
||||
|
||||
clock.tick(1);
|
||||
originalAndCallbackFiredOnce("exactly at the end of the timeout");
|
||||
});
|
||||
|
||||
test("executes only once, no matter how many times debounced function is called during the timeout", function() {
|
||||
debounced().then(callback);
|
||||
debounced().then(callback);
|
||||
|
||||
clock.tick(100);
|
||||
originalAndCallbackFiredOnce("(second call was supressed)");
|
||||
});
|
||||
|
||||
test("prolongs the timeout when the debounced function is called for the second time during the timeout", function() {
|
||||
debounced().then(callback);
|
||||
|
||||
clock.tick(50);
|
||||
debounced().then(callback);
|
||||
|
||||
clock.tick(50);
|
||||
nothingFired("at the end of the original timeout");
|
||||
|
||||
clock.tick(50);
|
||||
originalAndCallbackFiredOnce("exactly at the end of the prolonged timeout");
|
||||
});
|
||||
|
||||
test("preserves last call's context and params when executing delayed function", function() {
|
||||
var firstObj = {};
|
||||
var secondObj = {};
|
||||
|
||||
debounced.call(firstObj, "first");
|
||||
debounced.call(secondObj, "second");
|
||||
|
||||
clock.tick(100);
|
||||
ok(original.calledOn(secondObj), "the context of the second of two subsequent calls is preserved");
|
||||
ok(original.calledWithExactly("second"), "param passed during the second of two subsequent calls is preserved");
|
||||
});
|
||||
|
||||
test("can be called again after timeout passes", function() {
|
||||
debounced().then(callback);
|
||||
|
||||
clock.tick(100);
|
||||
debounced().then(callback);
|
||||
|
||||
clock.tick(100);
|
||||
ok(original.calledTwice, "original function is called for the second time");
|
||||
ok(callback.calledTwice, "debounced promise is resolved for the second time");
|
||||
});
|
||||
|
||||
test("passes resolved value from the original promise as a param to the debounced promise's callback", function() {
|
||||
originalPromiseResolvesWith = "original promise return value";
|
||||
debounced().then(callback);
|
||||
|
||||
clock.tick(100);
|
||||
ok(callback.calledWith("original promise return value"));
|
||||
});
|
@ -1,82 +0,0 @@
|
||||
var clock, original, debounced;
|
||||
|
||||
var firedOnce = function(message) {
|
||||
ok(original.calledOnce, message);
|
||||
};
|
||||
|
||||
var firedTwice = function(message) {
|
||||
ok(original.calledTwice, message);
|
||||
};
|
||||
|
||||
var notFired = function(message) {
|
||||
ok(!original.called, message);
|
||||
};
|
||||
|
||||
module("Discourse.debounce", {
|
||||
setup: function() {
|
||||
clock = sinon.useFakeTimers();
|
||||
original = sinon.spy();
|
||||
debounced = Discourse.debounce(original, 100);
|
||||
},
|
||||
|
||||
teardown: function() {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("delays function execution till the end of the timeout", function() {
|
||||
debounced();
|
||||
notFired("immediately after calling debounced function nothing happens");
|
||||
|
||||
clock.tick(99);
|
||||
notFired("just before the end of the timeout still nothing happens");
|
||||
|
||||
clock.tick(1);
|
||||
firedOnce("exactly at the end of the timeout the function is executed");
|
||||
});
|
||||
|
||||
test("executes delayed function only once, no matter how many times debounced function is called during the timeout", function() {
|
||||
debounced();
|
||||
debounced();
|
||||
|
||||
clock.tick(100);
|
||||
firedOnce("second call was supressed");
|
||||
});
|
||||
|
||||
test("prolongs the timeout when the debounced function is called for the second time during the timeout", function() {
|
||||
debounced();
|
||||
|
||||
clock.tick(50);
|
||||
debounced();
|
||||
|
||||
clock.tick(50);
|
||||
notFired("at the end of the original timeout nothing happens");
|
||||
|
||||
clock.tick(50);
|
||||
firedOnce("function is executed exactly at the end of the prolonged timeout");
|
||||
});
|
||||
|
||||
test("preserves last call's context and params when executing delayed function", function() {
|
||||
var firstObj = {};
|
||||
var secondObj = {};
|
||||
|
||||
debounced.call(firstObj, "first");
|
||||
debounced.call(secondObj, "second");
|
||||
|
||||
clock.tick(100);
|
||||
ok(original.calledOn(secondObj), "the context of the last of two subsequent calls is preserved");
|
||||
ok(original.calledWithExactly("second"), "param passed during the last of two subsequent calls is preserved");
|
||||
});
|
||||
|
||||
test("can be called again after timeout passes", function() {
|
||||
var firstObj = {};
|
||||
var secondObj = {};
|
||||
|
||||
debounced.call(firstObj, "first");
|
||||
|
||||
clock.tick(100);
|
||||
debounced.call(secondObj, "second");
|
||||
|
||||
clock.tick(100);
|
||||
firedTwice();
|
||||
});
|
@ -1,204 +0,0 @@
|
||||
var clock;
|
||||
|
||||
module("Discourse.Formatter", {
|
||||
setup: function() {
|
||||
clock = sinon.useFakeTimers(new Date(2012,11,31,12,0).getTime());
|
||||
},
|
||||
|
||||
teardown: function() {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
var format = "tiny";
|
||||
var leaveAgo = false;
|
||||
var mins_ago = function(mins){
|
||||
return new Date((new Date()) - mins * 60 * 1000);
|
||||
};
|
||||
|
||||
var formatMins = function(mins) {
|
||||
return Discourse.Formatter.relativeAge(mins_ago(mins), {format: format, leaveAgo: leaveAgo});
|
||||
};
|
||||
|
||||
var formatHours = function(hours) {
|
||||
return formatMins(hours * 60);
|
||||
};
|
||||
|
||||
var formatDays = function(days) {
|
||||
return formatHours(days * 24);
|
||||
};
|
||||
|
||||
var formatMonths = function(months) {
|
||||
return formatDays(months * 30);
|
||||
};
|
||||
|
||||
var shortDate = function(days){
|
||||
return moment().subtract('days', days).format('D MMM');
|
||||
};
|
||||
|
||||
test("formating medium length dates", function() {
|
||||
|
||||
format = "medium";
|
||||
var strip = function(html){
|
||||
return $(html).text();
|
||||
};
|
||||
|
||||
var shortDateYear = function(days){
|
||||
return moment().subtract('days', days).format('D MMM, YYYY');
|
||||
};
|
||||
|
||||
leaveAgo = true;
|
||||
equal(strip(formatMins(1.4)), "1 min ago");
|
||||
equal(strip(formatMins(2)), "2 mins ago");
|
||||
equal(strip(formatMins(56)), "56 mins ago");
|
||||
equal(strip(formatMins(57)), "1 hour ago");
|
||||
equal(strip(formatHours(4)), "4 hours ago");
|
||||
equal(strip(formatHours(22)), "22 hours ago");
|
||||
equal(strip(formatHours(23)), "1 day ago");
|
||||
equal(strip(formatDays(4.85)), "4 days ago");
|
||||
|
||||
leaveAgo = false;
|
||||
equal(strip(formatMins(0)), "just now");
|
||||
equal(strip(formatMins(1.4)), "1 min");
|
||||
equal(strip(formatMins(2)), "2 mins");
|
||||
equal(strip(formatMins(56)), "56 mins");
|
||||
equal(strip(formatMins(57)), "1 hour");
|
||||
equal(strip(formatHours(4)), "4 hours");
|
||||
equal(strip(formatHours(22)), "22 hours");
|
||||
equal(strip(formatHours(23)), "1 day");
|
||||
equal(strip(formatDays(4.85)), "4 days");
|
||||
|
||||
equal(strip(formatDays(6)), shortDate(6));
|
||||
equal(strip(formatDays(100)), shortDate(100)); // eg: 23 Jan
|
||||
equal(strip(formatDays(500)), shortDateYear(500));
|
||||
|
||||
equal($(formatDays(0)).attr("title"), moment().format('MMMM D, YYYY h:mma'));
|
||||
equal($(formatDays(0)).attr("class"), "date");
|
||||
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date(2012,0,9,12,0).getTime()); // Jan 9, 2012
|
||||
|
||||
equal(strip(formatDays(8)), shortDate(8));
|
||||
equal(strip(formatDays(10)), shortDateYear(10));
|
||||
|
||||
});
|
||||
|
||||
test("formating tiny dates", function() {
|
||||
var shortDateYear = function(days){
|
||||
return moment().subtract('days', days).format("D MMM 'YY");
|
||||
};
|
||||
|
||||
format = "tiny";
|
||||
equal(formatMins(0), "< 1m");
|
||||
equal(formatMins(2), "2m");
|
||||
equal(formatMins(60), "1h");
|
||||
equal(formatHours(4), "4h");
|
||||
equal(formatDays(1), "1d");
|
||||
equal(formatDays(14), "14d");
|
||||
equal(formatDays(15), shortDate(15));
|
||||
equal(formatDays(92), shortDate(92));
|
||||
equal(formatDays(364), shortDate(364));
|
||||
equal(formatDays(365), shortDate(365));
|
||||
equal(formatDays(366), shortDateYear(366)); // leap year
|
||||
equal(formatDays(500), shortDateYear(500));
|
||||
equal(formatDays(365*2 + 1), shortDateYear(365*2 + 1)); // one leap year
|
||||
|
||||
var originalValue = Discourse.SiteSettings.relative_date_duration;
|
||||
Discourse.SiteSettings.relative_date_duration = 7;
|
||||
equal(formatDays(7), "7d");
|
||||
equal(formatDays(8), shortDate(8));
|
||||
|
||||
Discourse.SiteSettings.relative_date_duration = 1;
|
||||
equal(formatDays(1), "1d");
|
||||
equal(formatDays(2), shortDate(2));
|
||||
|
||||
Discourse.SiteSettings.relative_date_duration = 0;
|
||||
equal(formatMins(0), "< 1m");
|
||||
equal(formatMins(2), "2m");
|
||||
equal(formatMins(60), "1h");
|
||||
equal(formatDays(1), shortDate(1));
|
||||
equal(formatDays(2), shortDate(2));
|
||||
equal(formatDays(366), shortDateYear(366));
|
||||
|
||||
Discourse.SiteSettings.relative_date_duration = null;
|
||||
equal(formatDays(1), '1d');
|
||||
equal(formatDays(14), '14d');
|
||||
equal(formatDays(15), shortDate(15));
|
||||
|
||||
Discourse.SiteSettings.relative_date_duration = 14;
|
||||
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date(2012,0,12,12,0).getTime()); // Jan 12, 2012
|
||||
|
||||
equal(formatDays(11), "11d");
|
||||
equal(formatDays(14), "14d");
|
||||
equal(formatDays(15), shortDateYear(15));
|
||||
equal(formatDays(366), shortDateYear(366));
|
||||
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date(2012,0,20,12,0).getTime()); // Jan 20, 2012
|
||||
|
||||
equal(formatDays(14), "14d");
|
||||
equal(formatDays(15), shortDate(15));
|
||||
equal(formatDays(20), shortDateYear(20));
|
||||
|
||||
Discourse.SiteSettings.relative_date_duration = originalValue;
|
||||
});
|
||||
|
||||
module("Discourse.Formatter");
|
||||
|
||||
test("autoUpdatingRelativeAge", function() {
|
||||
var d = moment().subtract('days',1).toDate();
|
||||
|
||||
var $elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d));
|
||||
equal($elem.data('format'), "tiny");
|
||||
equal($elem.data('time'), d.getTime());
|
||||
equal($elem.attr('title'), undefined);
|
||||
|
||||
$elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d, {title: true}));
|
||||
equal($elem.attr('title'), moment(d).longDate());
|
||||
|
||||
$elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d,{format: 'medium', title: true, leaveAgo: true}));
|
||||
equal($elem.data('format'), "medium-with-ago");
|
||||
equal($elem.data('time'), d.getTime());
|
||||
equal($elem.attr('title'), moment(d).longDate());
|
||||
equal($elem.html(), '1 day ago');
|
||||
|
||||
$elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d,{format: 'medium'}));
|
||||
equal($elem.data('format'), "medium");
|
||||
equal($elem.data('time'), d.getTime());
|
||||
equal($elem.attr('title'), undefined);
|
||||
equal($elem.html(), '1 day');
|
||||
});
|
||||
|
||||
test("updateRelativeAge", function(){
|
||||
|
||||
var d = new Date();
|
||||
var $elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d));
|
||||
$elem.data('time', d.getTime() - 2 * 60 * 1000);
|
||||
|
||||
Discourse.Formatter.updateRelativeAge($elem);
|
||||
|
||||
equal($elem.html(), "2m");
|
||||
|
||||
d = new Date();
|
||||
$elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d, {format: 'medium', leaveAgo: true}));
|
||||
$elem.data('time', d.getTime() - 2 * 60 * 1000);
|
||||
|
||||
Discourse.Formatter.updateRelativeAge($elem);
|
||||
|
||||
equal($elem.html(), "2 mins ago");
|
||||
});
|
||||
|
||||
test("breakUp", function(){
|
||||
|
||||
var b = function(s){ return Discourse.Formatter.breakUp(s,5); };
|
||||
|
||||
equal(b("hello"), "hello");
|
||||
equal(b("helloworld"), "hello world");
|
||||
equal(b("HeMans"), "He Mans");
|
||||
equal(b("he_man"), "he_ man");
|
||||
equal(b("he11111"), "he 11111");
|
||||
equal(b("HRCBob"), "HRC Bob");
|
||||
|
||||
});
|
@ -1,20 +0,0 @@
|
||||
var store = Discourse.KeyValueStore;
|
||||
|
||||
module("Discourse.KeyValueStore", {
|
||||
setup: function() {
|
||||
store.init("test");
|
||||
}
|
||||
});
|
||||
|
||||
test("it's able to get the result back from the store", function() {
|
||||
store.set({ key: "bob", value: "uncle" });
|
||||
equal(store.get("bob"), "uncle");
|
||||
});
|
||||
|
||||
test("is able to nuke the store", function() {
|
||||
store.set({ key: "bob1", value: "uncle" });
|
||||
store.abandonLocal();
|
||||
localStorage.a = 1;
|
||||
equal(store.get("bob1"), void 0);
|
||||
equal(localStorage.a, "1");
|
||||
});
|
@ -1,358 +0,0 @@
|
||||
/*global sanitizeHtml:true */
|
||||
|
||||
module("Discourse.Markdown", {
|
||||
setup: function() {
|
||||
Discourse.SiteSettings.traditional_markdown_linebreaks = false;
|
||||
}
|
||||
});
|
||||
|
||||
var cooked = function(input, expected, text) {
|
||||
var result = Discourse.Markdown.cook(input, {mentionLookup: false, sanitize: true});
|
||||
|
||||
if (result !== expected) {
|
||||
console.log(JSON.stringify(result));
|
||||
console.log(JSON.stringify(expected));
|
||||
}
|
||||
|
||||
equal(result, expected, text);
|
||||
};
|
||||
|
||||
var cookedOptions = function(input, opts, expected, text) {
|
||||
equal(Discourse.Markdown.cook(input, opts), expected, text);
|
||||
};
|
||||
|
||||
test("basic cooking", function() {
|
||||
cooked("hello", "<p>hello</p>", "surrounds text with paragraphs");
|
||||
cooked("**evil**", "<p><strong>evil</strong></p>", "it bolds text.");
|
||||
cooked("__bold__", "<p><strong>bold</strong></p>", "it bolds text.");
|
||||
cooked("*trout*", "<p><em>trout</em></p>", "it italicizes text.");
|
||||
cooked("_trout_", "<p><em>trout</em></p>", "it italicizes text.");
|
||||
cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once.");
|
||||
cooked("word_with_underscores", "<p>word_with_underscores</p>", "it doesn't do intraword italics");
|
||||
cooked("common/_special_font_face.html.erb", "<p>common/_special_font_face.html.erb</p>", "it doesn't intraword with a slash");
|
||||
cooked("hello \\*evil\\*", "<p>hello *evil*</p>", "it supports escaping of asterisks");
|
||||
cooked("hello \\_evil\\_", "<p>hello _evil_</p>", "it supports escaping of italics");
|
||||
cooked("brussel sproutes are *awful*.", "<p>brussel sproutes are <em>awful</em>.</p>", "it doesn't swallow periods.");
|
||||
});
|
||||
|
||||
test("Traditional Line Breaks", function() {
|
||||
var input = "1\n2\n3";
|
||||
cooked(input, "<p>1<br/>2<br/>3</p>", "automatically handles trivial newlines");
|
||||
|
||||
var traditionalOutput = "<p>1\n2\n3</p>";
|
||||
|
||||
cookedOptions(input,
|
||||
{traditional_markdown_linebreaks: true},
|
||||
traditionalOutput,
|
||||
"It supports traditional markdown via an option");
|
||||
|
||||
Discourse.SiteSettings.traditional_markdown_linebreaks = true;
|
||||
cooked(input, traditionalOutput, "It supports traditional markdown via a Site Setting");
|
||||
});
|
||||
|
||||
test("Line Breaks", function() {
|
||||
cooked("[] first choice\n[] second choice",
|
||||
"<p>[] first choice<br/>[] second choice</p>",
|
||||
"it handles new lines correctly with [] options");
|
||||
|
||||
cooked("<blockquote>evil</blockquote>\ntrout",
|
||||
"<blockquote>evil</blockquote>\n\n<p>trout</p>",
|
||||
"it doesn't insert <br> after blockquotes");
|
||||
|
||||
cooked("leading<blockquote>evil</blockquote>\ntrout",
|
||||
"leading<blockquote>evil</blockquote>\n\n<p>trout</p>",
|
||||
"it doesn't insert <br> after blockquotes with leading text");
|
||||
});
|
||||
|
||||
test("Paragraphs for HTML", function() {
|
||||
cooked("<div>hello world</div>", "<div>hello world</div>", "it doesn't surround <div> with paragraphs");
|
||||
cooked("<p>hello world</p>", "<p>hello world</p>", "it doesn't surround <p> with paragraphs");
|
||||
cooked("<i>hello world</i>", "<p><i>hello world</i></p>", "it surrounds inline <i> html tags with paragraphs");
|
||||
cooked("<b>hello world</b>", "<p><b>hello world</b></p>", "it surrounds inline <b> html tags with paragraphs");
|
||||
|
||||
});
|
||||
|
||||
test("Links", function() {
|
||||
|
||||
cooked("EvilTrout: http://eviltrout.com",
|
||||
'<p>EvilTrout: <a href="http://eviltrout.com">http://eviltrout.com</a></p>',
|
||||
"autolinks a URL");
|
||||
|
||||
cooked("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A",
|
||||
'<p>Youtube: <a href="http://www.youtube.com/watch?v=1MrpeBRkM5A">http://www.youtube.com/watch?v=1MrpeBRkM5A</a></p>',
|
||||
"allows links to contain query params");
|
||||
|
||||
cooked("Derpy: http://derp.com?__test=1",
|
||||
'<p>Derpy: <a href="http://derp.com?__test=1">http://derp.com?__test=1</a></p>',
|
||||
"works with double underscores in urls");
|
||||
|
||||
cooked("Derpy: http://derp.com?_test_=1",
|
||||
'<p>Derpy: <a href="http://derp.com?_test_=1">http://derp.com?_test_=1</a></p>',
|
||||
"works with underscores in urls");
|
||||
|
||||
cooked("Atwood: www.codinghorror.com",
|
||||
'<p>Atwood: <a href="http://www.codinghorror.com">www.codinghorror.com</a></p>',
|
||||
"autolinks something that begins with www");
|
||||
|
||||
cooked("Atwood: http://www.codinghorror.com",
|
||||
'<p>Atwood: <a href="http://www.codinghorror.com">http://www.codinghorror.com</a></p>',
|
||||
"autolinks a URL with http://www");
|
||||
|
||||
cooked("EvilTrout: http://eviltrout.com hello",
|
||||
'<p>EvilTrout: <a href="http://eviltrout.com">http://eviltrout.com</a> hello</p>',
|
||||
"autolinks with trailing text");
|
||||
|
||||
cooked("here is [an example](http://twitter.com)",
|
||||
'<p>here is <a href="http://twitter.com">an example</a></p>',
|
||||
"supports markdown style links");
|
||||
|
||||
cooked("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)",
|
||||
'<p>Batman: <a href="http://en.wikipedia.org/wiki/The_Dark_Knight_(film)">http://en.wikipedia.org/wiki/The_Dark_Knight_(film)</a></p>',
|
||||
"autolinks a URL with parentheses (like Wikipedia)");
|
||||
|
||||
cooked("Here's a tweet:\nhttps://twitter.com/evil_trout/status/345954894420787200",
|
||||
"<p>Here's a tweet:<br/><a href=\"https://twitter.com/evil_trout/status/345954894420787200\" class=\"onebox\" target=\"_blank\">https://twitter.com/evil_trout/status/345954894420787200</a></p>",
|
||||
"It doesn't strip the new line.");
|
||||
|
||||
cooked("1. View @eviltrout's profile here: http://meta.discourse.org/users/eviltrout/activity<br/>next line.",
|
||||
"<ol><li>View <span class=\"mention\">@eviltrout</span>'s profile here: <a href=\"http://meta.discourse.org/users/eviltrout/activity\">http://meta.discourse.org/users/eviltrout/activity</a><br>next line.</li></ol>",
|
||||
"allows autolinking within a list without inserting a paragraph.");
|
||||
|
||||
cooked("[3]: http://eviltrout.com", "", "It doesn't autolink markdown link references");
|
||||
|
||||
cooked("http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369",
|
||||
"<p><a href=\"http://discourse.org\">http://discourse.org</a> and " +
|
||||
"<a href=\"http://discourse.org/another_url\">http://discourse.org/another_url</a> and " +
|
||||
"<a href=\"http://www.imdb.com/name/nm2225369\">http://www.imdb.com/name/nm2225369</a></p>",
|
||||
'allows multiple links on one line');
|
||||
|
||||
cooked("* [Evil Trout][1]\n [1]: http://eviltrout.com",
|
||||
"<ul><li><a href=\"http://eviltrout.com\">Evil Trout</a></li></ul>",
|
||||
"allows markdown link references in a list");
|
||||
|
||||
cooked("User [MOD]: Hello!",
|
||||
"<p>User [MOD]: Hello!</p>",
|
||||
"It does not consider references that are obviously not URLs");
|
||||
});
|
||||
|
||||
test("simple quotes", function() {
|
||||
cooked("> nice!", "<blockquote><p>nice!</p></blockquote>", "it supports simple quotes");
|
||||
cooked(" > nice!", "<blockquote><p>nice!</p></blockquote>", "it allows quotes with preceeding spaces");
|
||||
cooked("> level 1\n> > level 2",
|
||||
"<blockquote><p>level 1</p><blockquote><p>level 2</p></blockquote></blockquote>",
|
||||
"it allows nesting of blockquotes");
|
||||
cooked("> level 1\n> > level 2",
|
||||
"<blockquote><p>level 1</p><blockquote><p>level 2</p></blockquote></blockquote>",
|
||||
"it allows nesting of blockquotes with spaces");
|
||||
|
||||
cooked("- hello\n\n > world\n > eviltrout",
|
||||
"<ul><li>hello</li></ul>\n\n<blockquote><p>world<br/>eviltrout</p></blockquote>",
|
||||
"it allows quotes within a list.");
|
||||
cooked(" > indent 1\n > indent 2", "<blockquote><p>indent 1<br/>indent 2</p></blockquote>", "allow multiple spaces to indent");
|
||||
|
||||
});
|
||||
|
||||
test("Quotes", function() {
|
||||
|
||||
cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n\nthird line[/quote]",
|
||||
{ topicId: 2 },
|
||||
"<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:</div><blockquote>" +
|
||||
"<p>a quote</p><p>second line</p><p>third line</p></blockquote></aside></p>",
|
||||
"works with multiple lines");
|
||||
|
||||
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
|
||||
{ topicId: 2, lookupAvatar: function(name) { return "" + name; }, sanitize: true },
|
||||
"<p>1</p>\n\n<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>bob" +
|
||||
"bob said:</div><blockquote><p>my quote</p></blockquote></aside></p>\n\n<p>2</p>",
|
||||
"handles quotes properly");
|
||||
|
||||
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
|
||||
{ topicId: 2, lookupAvatar: function(name) { } },
|
||||
"<p>1</p>\n\n<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>bob said:" +
|
||||
"</div><blockquote><p>my quote</p></blockquote></aside></p>\n\n<p>2</p>",
|
||||
"includes no avatar if none is found");
|
||||
});
|
||||
|
||||
test("Mentions", function() {
|
||||
|
||||
var alwaysTrue = { mentionLookup: (function() { return true; }) };
|
||||
|
||||
cookedOptions("Hello @sam", alwaysTrue,
|
||||
"<p>Hello <a class=\"mention\" href=\"/users/sam\">@sam</a></p>",
|
||||
"translates mentions to links");
|
||||
|
||||
cooked("Hello @EvilTrout", "<p>Hello <span class=\"mention\">@EvilTrout</span></p>", "adds a mention class");
|
||||
cooked("robin@email.host", "<p>robin@email.host</p>", "won't add mention class to an email address");
|
||||
cooked("hanzo55@yahoo.com", "<p>hanzo55@yahoo.com</p>", "won't be affected by email addresses that have a number before the @ symbol");
|
||||
cooked("@EvilTrout yo", "<p><span class=\"mention\">@EvilTrout</span> yo</p>", "it handles mentions at the beginning of a string");
|
||||
cooked("yo\n@EvilTrout", "<p>yo<br/><span class=\"mention\">@EvilTrout</span></p>", "it handles mentions at the beginning of a new line");
|
||||
cooked("`evil` @EvilTrout `trout`",
|
||||
"<p><code>evil</code> <span class=\"mention\">@EvilTrout</span> <code>trout</code></p>",
|
||||
"deals correctly with multiple <code> blocks");
|
||||
cooked("```\na @test\n```", "<p><pre><code class=\"lang-auto\">a @test</code></pre></p>", "should not do mentions within a code block.");
|
||||
|
||||
cooked("> foo bar baz @eviltrout",
|
||||
"<blockquote><p>foo bar baz <span class=\"mention\">@eviltrout</span></p></blockquote>",
|
||||
"handles mentions in simple quotes");
|
||||
|
||||
cooked("> foo bar baz @eviltrout ohmagerd\nlook at this",
|
||||
"<blockquote><p>foo bar baz <span class=\"mention\">@eviltrout</span> ohmagerd<br/>look at this</p></blockquote>",
|
||||
"does mentions properly with trailing text within a simple quote");
|
||||
|
||||
cooked("`code` is okay before @mention",
|
||||
"<p><code>code</code> is okay before <span class=\"mention\">@mention</span></p>",
|
||||
"Does not mention in an inline code block");
|
||||
|
||||
cooked("@mention is okay before `code`",
|
||||
"<p><span class=\"mention\">@mention</span> is okay before <code>code</code></p>",
|
||||
"Does not mention in an inline code block");
|
||||
|
||||
cooked("don't `@mention`",
|
||||
"<p>don't <code>@mention</code></p>",
|
||||
"Does not mention in an inline code block");
|
||||
|
||||
cooked("Yes `@this` should be code @eviltrout",
|
||||
"<p>Yes <code>@this</code> should be code <span class=\"mention\">@eviltrout</span></p>",
|
||||
"Does not mention in an inline code block");
|
||||
|
||||
cooked("@eviltrout and `@eviltrout`",
|
||||
"<p><span class=\"mention\">@eviltrout</span> and <code>@eviltrout</code></p>",
|
||||
"you can have a mention in an inline code block following a real mention.");
|
||||
|
||||
cooked("1. this is a list\n\n2. this is an @eviltrout mention\n",
|
||||
"<ol><li><p>this is a list</p></li><li><p>this is an <span class=\"mention\">@eviltrout</span> mention</p></li></ol>",
|
||||
"it mentions properly in a list.");
|
||||
|
||||
cookedOptions("@eviltrout", alwaysTrue,
|
||||
"<p><a class=\"mention\" href=\"/users/eviltrout\">@eviltrout</a></p>",
|
||||
"it doesn't onebox mentions");
|
||||
|
||||
});
|
||||
|
||||
|
||||
test("Heading", function() {
|
||||
cooked("**Bold**\n----------",
|
||||
"<h2><strong>Bold</strong></h2>",
|
||||
"It will bold the heading");
|
||||
});
|
||||
|
||||
test("Oneboxing", function() {
|
||||
|
||||
var matches = function(input, regexp) {
|
||||
return Discourse.Markdown.cook(input, {mentionLookup: false }).match(regexp);
|
||||
};
|
||||
|
||||
ok(!matches("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org", /onebox/),
|
||||
"doesn't onebox a link within a list");
|
||||
|
||||
ok(matches("http://test.com", /onebox/), "adds a onebox class to a link on its own line");
|
||||
ok(matches("http://test.com\nhttp://test2.com", /onebox[\s\S]+onebox/m), "supports multiple links");
|
||||
ok(!matches("http://test.com bob", /onebox/), "doesn't onebox links that have trailing text");
|
||||
|
||||
ok(!matches("[Tom Cruise](http://www.tomcruise.com/)", "onebox"), "Markdown links with labels are not oneboxed");
|
||||
ok(matches("[http://www.tomcruise.com/](http://www.tomcruise.com/)",
|
||||
"onebox"),
|
||||
"Markdown links where the label is the same as the url are oneboxed");
|
||||
|
||||
cooked("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street",
|
||||
"<p><a href=\"http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street\" class=\"onebox\"" +
|
||||
" target=\"_blank\">http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street</a></p>",
|
||||
"works with links that have underscores in them");
|
||||
|
||||
});
|
||||
|
||||
test("Code Blocks", function() {
|
||||
|
||||
cooked("```\na\nb\nc\n\nd\n```",
|
||||
"<p><pre><code class=\"lang-auto\">a\nb\nc\n\nd</code></pre></p>",
|
||||
"it treats new lines properly");
|
||||
|
||||
cooked("```\ntest\n```",
|
||||
"<p><pre><code class=\"lang-auto\">test</code></pre></p>",
|
||||
"it supports basic code blocks");
|
||||
|
||||
cooked("```json\n{hello: 'world'}\n```\ntrailing",
|
||||
"<p><pre><code class=\"json\">{hello: 'world'}</code></pre></p>\n\n<p>trailing</p>",
|
||||
"It does not truncate text after a code block.");
|
||||
|
||||
cooked("```json\nline 1\n\nline 2\n\n\nline3\n```",
|
||||
"<p><pre><code class=\"json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
||||
"it maintains new lines inside a code block.");
|
||||
|
||||
cooked("hello\nworld\n```json\nline 1\n\nline 2\n\n\nline3\n```",
|
||||
"<p>hello<br/>world<br/></p>\n\n<p><pre><code class=\"json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
||||
"it maintains new lines inside a code block with leading content.");
|
||||
|
||||
cooked("```text\n<header>hello</header>\n```",
|
||||
"<p><pre><code class=\"text\"><header>hello</header></code></pre></p>",
|
||||
"it escapes code in the code block");
|
||||
|
||||
cooked("```ruby\n# cool\n```",
|
||||
"<p><pre><code class=\"ruby\"># cool</code></pre></p>",
|
||||
"it supports changing the language");
|
||||
|
||||
cooked(" ```\n hello\n ```",
|
||||
"<pre><code>```\nhello\n```</code></pre>",
|
||||
"only detect ``` at the begining of lines");
|
||||
|
||||
cooked("```ruby\ndef self.parse(text)\n\n text\nend\n```",
|
||||
"<p><pre><code class=\"ruby\">def self.parse(text)\n\n text\nend</code></pre></p>",
|
||||
"it allows leading spaces on lines in a code block.");
|
||||
|
||||
cooked("```ruby\nhello `eviltrout`\n```",
|
||||
"<p><pre><code class=\"ruby\">hello `eviltrout`</code></pre></p>",
|
||||
"it allows code with backticks in it");
|
||||
|
||||
cooked("```eviltrout\nhello\n```",
|
||||
"<p><pre><code class=\"lang-auto\">hello</code></pre></p>",
|
||||
"it doesn't not whitelist all classes");
|
||||
|
||||
cooked("```[quote=\"sam, post:1, topic:9441, full:true\"]This is `<not>` a bug.[/quote]```",
|
||||
"<p><pre><code class=\"lang-auto\">[quote="sam, post:1, topic:9441, full:true"]This is `<not>` a bug.[/quote]</code></pre></p>",
|
||||
"it allows code with backticks in it");
|
||||
|
||||
});
|
||||
|
||||
test("sanitize", function() {
|
||||
var sanitize = Discourse.Markdown.sanitize;
|
||||
|
||||
equal(sanitize("<i class=\"icon-bug icon-spin\">bug</i>"), "<i>bug</i>");
|
||||
equal(sanitize("<div><script>alert('hi');</script></div>"), "<div></div>");
|
||||
equal(sanitize("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p>hello</p></div>");
|
||||
cooked("hello<script>alert(42)</script>", "<p>hello</p>", "it sanitizes while cooking");
|
||||
|
||||
cooked("<a href='http://disneyland.disney.go.com/'>disney</a> <a href='http://reddit.com'>reddit</a>",
|
||||
"<p><a href=\"http://disneyland.disney.go.com/\">disney</a> <a href=\"http://reddit.com\">reddit</a></p>",
|
||||
"we can embed proper links");
|
||||
|
||||
cooked("<table><tr><td>hello</td></tr></table>\nafter", "<p>after</p>", "it does not allow tables");
|
||||
cooked("<blockquote>a\n</blockquote>\n", "<blockquote>a\n\n<br/>\n\n</blockquote>", "it does not double sanitize");
|
||||
});
|
||||
|
||||
test("URLs in BBCode tags", function() {
|
||||
|
||||
cooked("[img]http://eviltrout.com/eviltrout.png[/img][img]http://samsaffron.com/samsaffron.png[/img]",
|
||||
"<p><img src=\"http://eviltrout.com/eviltrout.png\"/><img src=\"http://samsaffron.com/samsaffron.png\"/></p>",
|
||||
"images are properly parsed");
|
||||
|
||||
cooked("[url]http://discourse.org[/url]",
|
||||
"<p><a href=\"http://discourse.org\">http://discourse.org</a></p>",
|
||||
"links are properly parsed");
|
||||
|
||||
cooked("[url=http://discourse.org]discourse[/url]",
|
||||
"<p><a href=\"http://discourse.org\">discourse</a></p>",
|
||||
"named links are properly parsed");
|
||||
|
||||
});
|
||||
|
||||
test("urlAllowed", function() {
|
||||
var allowed = function(url, msg) {
|
||||
equal(Discourse.Markdown.urlAllowed(url), url, msg);
|
||||
};
|
||||
|
||||
allowed("/foo/bar.html", "allows relative urls");
|
||||
allowed("http://eviltrout.com/evil/trout", "allows full urls");
|
||||
allowed("https://eviltrout.com/evil/trout", "allows https urls");
|
||||
allowed("//eviltrout.com/evil/trout", "allows protocol relative urls");
|
||||
|
||||
});
|
@ -1,23 +0,0 @@
|
||||
module("Discourse.Onebox", {
|
||||
setup: function() {
|
||||
this.anchor = $("<a href='http://bla.com'></a>")[0];
|
||||
}
|
||||
});
|
||||
|
||||
asyncTestDiscourse("Stops rapid calls with cache true", function() {
|
||||
this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
|
||||
Discourse.Onebox.load(this.anchor, true);
|
||||
Discourse.Onebox.load(this.anchor, true);
|
||||
|
||||
start();
|
||||
ok(Discourse.ajax.calledOnce);
|
||||
});
|
||||
|
||||
asyncTestDiscourse("Stops rapid calls with cache true", function() {
|
||||
this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
|
||||
Discourse.Onebox.load(this.anchor, false);
|
||||
Discourse.Onebox.load(this.anchor, false);
|
||||
|
||||
start();
|
||||
ok(Discourse.ajax.calledOnce);
|
||||
});
|
@ -1,71 +0,0 @@
|
||||
module("Discourse.PreloadStore", {
|
||||
setup: function() {
|
||||
PreloadStore.store('bane', 'evil');
|
||||
}
|
||||
});
|
||||
|
||||
test("get", function() {
|
||||
blank(PreloadStore.get('joker'), "returns blank for a missing key");
|
||||
equal(PreloadStore.get('bane'), 'evil', "returns the value for that key");
|
||||
});
|
||||
|
||||
test("remove", function() {
|
||||
PreloadStore.remove('bane');
|
||||
blank(PreloadStore.get('bane'), "removes the value if the key exists");
|
||||
});
|
||||
|
||||
asyncTestDiscourse("getAndRemove returns a promise that resolves to null", function() {
|
||||
expect(1);
|
||||
|
||||
PreloadStore.getAndRemove('joker').then(function(result) {
|
||||
blank(result);
|
||||
start();
|
||||
});
|
||||
});
|
||||
|
||||
asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder", function() {
|
||||
expect(1);
|
||||
|
||||
var finder = function() { return 'batdance'; };
|
||||
PreloadStore.getAndRemove('joker', finder).then(function(result) {
|
||||
equal(result, 'batdance');
|
||||
start();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder's promise", function() {
|
||||
expect(1);
|
||||
|
||||
var finder = function() {
|
||||
return Ember.Deferred.promise(function(promise) { promise.resolve('hahahah'); });
|
||||
};
|
||||
|
||||
PreloadStore.getAndRemove('joker', finder).then(function(result) {
|
||||
equal(result, 'hahahah');
|
||||
start();
|
||||
});
|
||||
});
|
||||
|
||||
asyncTestDiscourse("returns a promise that rejects with the result of the finder's rejected promise", function() {
|
||||
expect(1);
|
||||
|
||||
var finder = function() {
|
||||
return Ember.Deferred.promise(function(promise) { promise.reject('error'); });
|
||||
};
|
||||
|
||||
PreloadStore.getAndRemove('joker', finder).then(null, function(result) {
|
||||
equal(result, 'error');
|
||||
start();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
asyncTestDiscourse("returns a promise that resolves to 'evil'", function() {
|
||||
expect(1);
|
||||
|
||||
PreloadStore.getAndRemove('bane').then(function(result) {
|
||||
equal(result, 'evil');
|
||||
start();
|
||||
});
|
||||
});
|
@ -1,147 +0,0 @@
|
||||
module("Discourse.Utilities");
|
||||
|
||||
var utils = Discourse.Utilities;
|
||||
|
||||
test("emailValid", function() {
|
||||
ok(utils.emailValid('Bob@example.com'), "allows upper case in the first part of emails");
|
||||
ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain");
|
||||
});
|
||||
|
||||
var validUpload = utils.validateUploadedFiles;
|
||||
|
||||
test("validateUploadedFiles", function() {
|
||||
ok(!validUpload(null), "no files are invalid");
|
||||
ok(!validUpload(undefined), "undefined files are invalid");
|
||||
ok(!validUpload([]), "empty array of files is invalid");
|
||||
});
|
||||
|
||||
test("uploading one file", function() {
|
||||
this.stub(bootbox, "alert");
|
||||
|
||||
ok(!validUpload([1, 2]));
|
||||
ok(bootbox.alert.calledWith(I18n.t('post.errors.too_many_uploads')));
|
||||
});
|
||||
|
||||
test("new user cannot upload images", function() {
|
||||
Discourse.SiteSettings.newuser_max_images = 0;
|
||||
this.stub(bootbox, "alert");
|
||||
|
||||
ok(!validUpload([{name: "image.png"}]));
|
||||
ok(bootbox.alert.calledWith(I18n.t('post.errors.image_upload_not_allowed_for_new_user')));
|
||||
});
|
||||
|
||||
test("new user cannot upload attachments", function() {
|
||||
Discourse.SiteSettings.newuser_max_attachments = 0;
|
||||
this.stub(bootbox, "alert");
|
||||
|
||||
ok(!validUpload([{name: "roman.txt"}]));
|
||||
ok(bootbox.alert.calledWith(I18n.t('post.errors.attachment_upload_not_allowed_for_new_user')));
|
||||
});
|
||||
|
||||
test("ensures an authorized upload", function() {
|
||||
var html = { name: "unauthorized.html" };
|
||||
var extensions = Discourse.SiteSettings.authorized_extensions.replace(/\|/g, ", ");
|
||||
this.stub(bootbox, "alert");
|
||||
|
||||
ok(!validUpload([html]));
|
||||
ok(bootbox.alert.calledWith(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: extensions })));
|
||||
});
|
||||
|
||||
test("prevents files that are too big from being uploaded", function() {
|
||||
var image = { name: "image.png", size: 10 * 1024 };
|
||||
Discourse.SiteSettings.max_image_size_kb = 5;
|
||||
Discourse.User.currentProp("trust_level", 1);
|
||||
this.stub(bootbox, "alert");
|
||||
|
||||
ok(!validUpload([image]));
|
||||
ok(bootbox.alert.calledWith(I18n.t('post.errors.image_too_large', { max_size_kb: 5 })));
|
||||
});
|
||||
|
||||
var dummyBlob = function() {
|
||||
var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
|
||||
if (BlobBuilder) {
|
||||
var bb = new BlobBuilder();
|
||||
bb.append([1]);
|
||||
return bb.getBlob("image/png");
|
||||
} else {
|
||||
return new Blob([1], { "type" : "image\/png" });
|
||||
}
|
||||
};
|
||||
|
||||
test("allows valid uploads to go through", function() {
|
||||
Discourse.User.currentProp("trust_level", 1);
|
||||
Discourse.SiteSettings.max_image_size_kb = 15;
|
||||
this.stub(bootbox, "alert");
|
||||
|
||||
// image
|
||||
var image = { name: "image.png", size: 10 * 1024 };
|
||||
ok(validUpload([image]));
|
||||
// pasted image
|
||||
var pastedImage = dummyBlob();
|
||||
ok(validUpload([pastedImage]));
|
||||
|
||||
ok(!bootbox.alert.calledOnce);
|
||||
});
|
||||
|
||||
var getUploadMarkdown = function(filename) {
|
||||
return utils.getUploadMarkdown({
|
||||
original_filename: filename,
|
||||
filesize: 42,
|
||||
width: 100,
|
||||
height: 200,
|
||||
url: "/uploads/123/abcdef.ext"
|
||||
});
|
||||
};
|
||||
|
||||
test("getUploadMarkdown", function() {
|
||||
ok(getUploadMarkdown("lolcat.gif") === '<img src="/uploads/123/abcdef.ext" width="100" height="200">');
|
||||
ok(getUploadMarkdown("important.txt") === '<a class="attachment" href="/uploads/123/abcdef.ext">important.txt</a> (42 Bytes)');
|
||||
});
|
||||
|
||||
test("isAnImage", function() {
|
||||
_.each(["png", "jpg", "jpeg", "bmp", "gif", "tif", "tiff"], function(extension) {
|
||||
var image = "image." + extension;
|
||||
ok(utils.isAnImage(image), image + " is recognized as an image");
|
||||
ok(utils.isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image");
|
||||
});
|
||||
ok(!utils.isAnImage("file.txt"));
|
||||
ok(!utils.isAnImage("http://foo.bar/path/to/file.txt"));
|
||||
ok(!utils.isAnImage(""));
|
||||
});
|
||||
|
||||
test("avatarUrl", function() {
|
||||
blank(Discourse.Utilities.avatarUrl('', 'tiny'), "no template returns blank");
|
||||
equal(Discourse.Utilities.avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/20.png", "simple avatar url");
|
||||
equal(Discourse.Utilities.avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/45.png", "different size");
|
||||
});
|
||||
|
||||
test("avatarImg", function() {
|
||||
var avatarTemplate = "/path/to/avatar/{size}.png";
|
||||
equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}),
|
||||
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar'>",
|
||||
"it returns the avatar html");
|
||||
|
||||
equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}),
|
||||
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar' title='evilest trout'>",
|
||||
"it adds a title if supplied");
|
||||
|
||||
equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}),
|
||||
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar evil fish'>",
|
||||
"it adds extra classes if supplied");
|
||||
|
||||
blank(Discourse.Utilities.avatarImg({avatarTemplate: "", size: 'tiny'}),
|
||||
"it doesn't render avatars for invalid avatar template");
|
||||
});
|
||||
|
||||
module("Discourse.Utilities.cropAvatar with animated avatars", {
|
||||
setup: function() { Discourse.SiteSettings.allow_animated_avatars = true; }
|
||||
});
|
||||
|
||||
asyncTestDiscourse("cropAvatar", function() {
|
||||
expect(1);
|
||||
|
||||
Discourse.Utilities.cropAvatar("/path/to/avatar.gif", "image/gif").then(function(avatarTemplate) {
|
||||
equal(avatarTemplate, "/path/to/avatar.gif", "returns the url to the gif when animated gif are enabled");
|
||||
start();
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user