Convert Discourse.Post to ES6 and use Store model

- Includes acceptance tests for composer (post, edit)
- Supports acceptance testing of bootbox
This commit is contained in:
Robin Ward
2015-04-01 14:18:46 -04:00
parent 19a9a8b408
commit 22ffcba8e6
19 changed files with 747 additions and 440 deletions
@@ -0,0 +1,117 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Composer", { loggedIn: true });
test("Tests the Composer controls", () => {
visit("/");
andThen(() => {
ok(exists('#create-topic'), 'the create button is visible');
});
click('#create-topic');
andThen(() => {
ok(exists('#wmd-input'), 'the composer input is visible');
ok(exists('.title-input .popup-tip.bad.hide'), 'title errors are hidden by default');
ok(exists('.textarea-wrapper .popup-tip.bad.hide'), 'body errors are hidden by default');
});
click('a.toggle-preview');
andThen(() => {
ok(!exists('#wmd-preview:visible'), "clicking the toggle hides the preview");
});
click('a.toggle-preview');
andThen(() => {
ok(exists('#wmd-preview:visible'), "clicking the toggle shows the preview again");
});
click('#reply-control button.create');
andThen(() => {
ok(!exists('.title-input .popup-tip.bad.hide'), 'it shows the empty title error');
ok(!exists('.textarea-wrapper .popup-tip.bad.hide'), 'it shows the empty body error');
});
fillIn('#reply-title', "this is my new topic title");
andThen(() => {
ok(exists('.title-input .popup-tip.good'), 'the title is now good');
});
fillIn('#wmd-input', "this is the *content* of a post");
andThen(() => {
equal(find('#wmd-preview').html(), "<p>this is the <em>content</em> of a post</p>", "it previews content");
ok(exists('.textarea-wrapper .popup-tip.good'), 'the body is now good');
});
click('#reply-control a.cancel');
andThen(() => {
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
});
click('.modal-footer a:eq(1)');
andThen(() => {
ok(!exists('.bootbox.modal'), 'the confirmation can be cancelled');
});
});
test("Create a topic with server side errors", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "this title triggers an error");
fillIn('#wmd-input', "this is the *content* of a post");
click('#reply-control button.create');
andThen(() => {
ok(exists('.bootbox.modal'), 'it pops up an error message');
});
click('.bootbox.modal a.btn-primary');
andThen(() => {
ok(!exists('.bootbox.modal'), 'it dismisses the error');
ok(exists('#wmd-input'), 'the composer input is visible');
});
});
test("Create a Topic", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "Internationalization Localization");
fillIn('#wmd-input', "this is the *content* of a new topic post");
click('#reply-control button.create');
andThen(() => {
equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL");
});
});
test("Create a Reply", () => {
visit("/t/internationalization-localization/280");
click('#topic-footer-buttons .btn.create');
andThen(() => {
ok(exists('#wmd-input'), 'the composer input is visible');
ok(!exists('#reply-title'), 'there is no title since this is a reply');
});
fillIn('#wmd-input', 'this is the content of my reply');
click('#reply-control button.create');
andThen(() => {
exists('#post_12345', 'it inserts the post into the document');
});
});
test("Edit the first post", () => {
visit("/t/internationalization-localization/280");
click('.topic-post:eq(0) button[data-action=showMoreActions]');
click('.topic-post:eq(0) button[data-action=edit]');
andThen(() => {
equal(find('#wmd-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text');
});
fillIn('#wmd-input', "This is the new text for the post");
fillIn('#reply-title', "This is the new text for the title");
click('#reply-control button.create');
andThen(() => {
ok(!exists('#wmd-input'), 'it closes the composer');
ok(find('#topic-title h1').text().indexOf('This is the new text for the title') !== -1, 'it shows the new title');
ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post');
});
});
@@ -1,10 +1,6 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Header (Staff)", {
user: { username: 'test',
staff: true,
site_flagged_posts_count: 1 }
});
acceptance("Header (Staff)", { loggedIn: true });
test("header", () => {
visit("/");
+4
View File
@@ -0,0 +1,4 @@
export default {
"/posts/398": {"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"<p>Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?</p>","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":2,"can_edit":true,"can_delete":false,"can_recover":true,"user_title":null,"raw":"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}
};
@@ -0,0 +1,4 @@
export default {
"/session/current.json": {"current_user":{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/localhost/eviltrout/{size}/5275.png","name":"Robin Ward","total_unread_notifications":205,"unread_notifications":0,"unread_private_messages":0,"admin":true,"notification_channel_position":null,"site_flagged_posts_count":1,"moderator":true,"staff":true,"title":"co-founder","reply_count":859,"topic_count":36,"enable_quoting":true,"external_links_in_new_tab":false,"dynamic_favicon":true,"trust_level":4,"can_edit":true,"can_invite_to_forum":true,"should_be_redirected_to_top":false,"disable_jump_reply":false,"custom_fields":{},"muted_category_ids":[],"dismissed_banner_key":null,"akismet_review_count":0}}
};
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@ function parsePostData(query) {
const result = {};
query.split("&").forEach(function(part) {
const item = part.split("=");
result[item[0]] = decodeURIComponent(item[1]);
result[item[0]] = decodeURIComponent(item[1]).replace(/\+/g, ' ');
});
return result;
}
@@ -33,9 +33,16 @@ const _moreWidgets = [
{id: 224, name: 'Good Repellant'}
];
function loggedIn() {
return !!Discourse.User.current();
}
export default function() {
const server = new Pretender(function() {
const fixturesByUrl = {};
// Load any fixtures automatically
const self = this;
Ember.keys(require._eak_seen).forEach(function(entry) {
@@ -44,6 +51,7 @@ export default function() {
if (fixture && fixture.default) {
const obj = fixture.default;
Ember.keys(obj).forEach(function(url) {
fixturesByUrl[url] = obj[url];
self.get(url, function() {
return response(obj[url]);
});
@@ -52,6 +60,20 @@ export default function() {
}
});
this.get('/composer-messages', () => { return response([]); });
this.get("/latest.json", () => {
const json = fixturesByUrl['/latest.json'];
if (loggedIn()) {
// Stuff to let us post
json.topic_list.can_create_topic = true;
json.topic_list.draft_key = "new_topic";
json.topic_list.draft_sequence = 1;
}
return response(json);
});
this.get("/t/id_for/:slug", function() {
return response({id: 280, slug: "internationalization-localization", url: "/t/internationalization-localization/280"});
});
@@ -99,6 +121,33 @@ export default function() {
this.delete('/posts/:post_id', success);
this.put('/posts/:post_id/recover', success);
this.put('/posts/:post_id', (request) => {
return response({ post: {id: request.params.post_id, version: 2 } });
});
this.put('/t/:slug/:id', (request) => {
const data = parsePostData(request.requestBody);
return response(200, { basic_topic: {id: request.params.id,
title: data.title,
fancy_title: data.title,
slug: request.params.slug } })
});
this.post('/posts', function(request) {
const data = parsePostData(request.requestBody);
if (data.title === "this title triggers an error") {
return response(422, {errors: ['That title has already been taken']});
} else {
return response(200, {
success: true,
action: 'create_post',
post: {id: 12345, topic_id: 280, topic_slug: 'internationalization-localization'}
});
}
});
this.get('/widgets/:widget_id', function(request) {
const w = _widgets.findBy('id', parseInt(request.params.widget_id));
if (w) {
@@ -130,8 +179,11 @@ export default function() {
});
this.delete('/widgets/:widget_id', success);
});
this.post('/topics/timings', function() {
return response(200, {});
});
});
server.prepareBody = function(body){
if (body && typeof body === "object") {
+43 -3
View File
@@ -1,20 +1,59 @@
/* global asyncTest */
import sessionFixtures from 'fixtures/session-fixtures';
import siteFixtures from 'fixtures/site_fixtures';
import HeaderView from 'discourse/views/header';
function currentUser() {
return Discourse.User.create(sessionFixtures['/session/current.json'].current_user);
}
function logIn() {
Discourse.User.resetCurrent(currentUser());
}
const Plugin = $.fn.modal;
const Modal = Plugin.Constructor;
function AcceptanceModal(option, _relatedTarget) {
return this.each(function () {
var $this = $(this);
var data = $this.data('bs.modal');
var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option === 'object' && option);
if (!data) $this.data('bs.modal', (data = new Modal(this, options)));
data.$body = $('#ember-testing');
if (typeof option === 'string') data[option](_relatedTarget);
else if (options.show) data.show(_relatedTarget);
});
}
window.bootbox.$body = $('#ember-testing');
$.fn.modal = AcceptanceModal;
var oldAvatar = Discourse.Utilities.avatarImg;
function acceptance(name, options) {
module("Acceptance: " + name, {
setup: function() {
Ember.run(Discourse, Discourse.advanceReadiness);
// Don't render avatars in acceptance tests, it's faster and no 404s
Discourse.Utilities.avatarImg = () => "";
// For now don't do scrolling stuff in Test Mode
Ember.CloakedCollectionView.scrolled = Ember.K;
HeaderView.reopen({examineDockHeader: Ember.K});
var siteJson = siteFixtures['site.json'].site;
if (options) {
if (options.setup) {
options.setup.call(this);
}
if (options.user) {
Discourse.User.resetCurrent(Discourse.User.create(options.user));
if (options.loggedIn) {
logIn();
}
if (options.settings) {
@@ -34,6 +73,7 @@ function acceptance(name, options) {
options.teardown.call(this);
}
Discourse.Utilities.avatarImg = oldAvatar;
Discourse.reset();
}
});
@@ -61,4 +101,4 @@ function fixture(selector) {
return $("#qunit-fixture");
}
export { acceptance, controllerFor, asyncTestDiscourse, fixture };
export { acceptance, controllerFor, asyncTestDiscourse, fixture, logIn, currentUser };
+38 -49
View File
@@ -1,16 +1,18 @@
module("Discourse.Composer", {
setup: function() {
sandbox.stub(Discourse.User, 'currentProp').withArgs('admin').returns(false);
},
import { currentUser } from 'helpers/qunit-helpers';
teardown: function() {
Discourse.User.currentProp.restore();
}
});
module("model:composer");
function createComposer(opts) {
opts = opts || {};
opts.user = opts.user || currentUser();
opts.site = Discourse.Site.current();
opts.siteSettings = Discourse.SiteSettings;
return Discourse.Composer.create(opts);
}
test('replyLength', function() {
var replyLength = function(val, expectedLength) {
var composer = Discourse.Composer.create({ reply: val });
const replyLength = function(val, expectedLength) {
const composer = createComposer({ reply: val });
equal(composer.get('replyLength'), expectedLength);
};
@@ -23,8 +25,8 @@ test('replyLength', function() {
test('missingReplyCharacters', function() {
Discourse.SiteSettings.min_first_post_length = 40;
var missingReplyCharacters = function(val, isPM, isFirstPost, expected, message) {
var composer = Discourse.Composer.create({ reply: val, creatingPrivateMessage: isPM, creatingTopic: isFirstPost });
const missingReplyCharacters = function(val, isPM, isFirstPost, expected, message) {
const composer = createComposer({ reply: val, creatingPrivateMessage: isPM, creatingTopic: isFirstPost });
equal(composer.get('missingReplyCharacters'), expected, message);
};
@@ -34,8 +36,8 @@ test('missingReplyCharacters', function() {
});
test('missingTitleCharacters', function() {
var missingTitleCharacters = function(val, isPM, expected, message) {
var composer = Discourse.Composer.create({ title: val, creatingPrivateMessage: isPM });
const missingTitleCharacters = function(val, isPM, expected, message) {
const composer = createComposer({ title: val, creatingPrivateMessage: isPM });
equal(composer.get('missingTitleCharacters'), expected, message);
};
@@ -44,7 +46,7 @@ test('missingTitleCharacters', function() {
});
test('replyDirty', function() {
var composer = Discourse.Composer.create();
const composer = createComposer();
ok(!composer.get('replyDirty'), "by default it's false");
composer.setProperties({
@@ -58,7 +60,7 @@ test('replyDirty', function() {
});
test("appendText", function() {
var composer = Discourse.Composer.create();
const composer = createComposer();
blank(composer.get('reply'), "the reply is blank by default");
@@ -89,7 +91,7 @@ test("appendText", function() {
test("Title length for regular topics", function() {
Discourse.SiteSettings.min_topic_title_length = 5;
Discourse.SiteSettings.max_topic_title_length = 10;
var composer = Discourse.Composer.create();
const composer = createComposer();
composer.set('title', 'asdf');
ok(!composer.get('titleLengthValid'), "short titles are not valid");
@@ -104,7 +106,7 @@ test("Title length for regular topics", function() {
test("Title length for private messages", function() {
Discourse.SiteSettings.min_private_message_title_length = 5;
Discourse.SiteSettings.max_topic_title_length = 10;
var composer = Discourse.Composer.create({action: Discourse.Composer.PRIVATE_MESSAGE});
const composer = createComposer({action: Discourse.Composer.PRIVATE_MESSAGE});
composer.set('title', 'asdf');
ok(!composer.get('titleLengthValid'), "short titles are not valid");
@@ -119,7 +121,7 @@ test("Title length for private messages", function() {
test("Title length for private messages", function() {
Discourse.SiteSettings.min_private_message_title_length = 5;
Discourse.SiteSettings.max_topic_title_length = 10;
var composer = Discourse.Composer.create({action: Discourse.Composer.PRIVATE_MESSAGE});
const composer = createComposer({action: Discourse.Composer.PRIVATE_MESSAGE});
composer.set('title', 'asdf');
ok(!composer.get('titleLengthValid'), "short titles are not valid");
@@ -132,10 +134,10 @@ test("Title length for private messages", function() {
});
test('editingFirstPost', function() {
var composer = Discourse.Composer.create();
const composer = createComposer();
ok(!composer.get('editingFirstPost'), "it's false by default");
var post = Discourse.Post.create({id: 123, post_number: 2});
const post = Discourse.Post.create({id: 123, post_number: 2});
composer.setProperties({post: post, action: Discourse.Composer.EDIT });
ok(!composer.get('editingFirstPost'), "it's false when not editing the first post");
@@ -145,7 +147,7 @@ test('editingFirstPost', function() {
});
test('clearState', function() {
var composer = Discourse.Composer.create({
const composer = createComposer({
originalText: 'asdf',
reply: 'asdf2',
post: Discourse.Post.create({id: 1}),
@@ -163,61 +165,48 @@ test('clearState', function() {
test('initial category when uncategorized is allowed', function() {
Discourse.SiteSettings.allow_uncategorized_topics = true;
var composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1});
const composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1});
equal(composer.get('categoryId'),undefined,"Uncategorized by default");
});
test('initial category when uncategorized is not allowed', function() {
Discourse.SiteSettings.allow_uncategorized_topics = false;
var composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1});
const composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1});
ok(composer.get('categoryId') === undefined, "Uncategorized by default. Must choose a category.");
});
test('showPreview', function() {
var new_composer = function() {
const newComposer = function() {
return Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1});
};
Discourse.Mobile.mobileView = true;
equal(new_composer().get('showPreview'), false, "Don't show preview in mobile view");
equal(newComposer().get('showPreview'), false, "Don't show preview in mobile view");
Discourse.KeyValueStore.set({ key: 'composer.showPreview', value: 'true' });
equal(new_composer().get('showPreview'), false, "Don't show preview in mobile view even if KeyValueStore wants to");
equal(newComposer().get('showPreview'), false, "Don't show preview in mobile view even if KeyValueStore wants to");
Discourse.KeyValueStore.remove('composer.showPreview');
Discourse.Mobile.mobileView = false;
equal(new_composer().get('showPreview'), true, "Show preview by default in desktop view");
equal(newComposer().get('showPreview'), true, "Show preview by default in desktop view");
});
test('open with a quote', function() {
var quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]';
var new_composer = function() {
const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]';
const newComposer = function() {
return Discourse.Composer.open({action: Discourse.Composer.REPLY, draftKey: 'asfd', draftSequence: 1, quote: quote});
};
equal(new_composer().get('originalText'), quote, "originalText is the quote" );
equal(new_composer().get('replyDirty'), false, "replyDirty is initally false with a quote" );
});
module("Discourse.Composer as admin", {
setup: function() {
Discourse.SiteSettings.min_topic_title_length = 5;
Discourse.SiteSettings.max_topic_title_length = 10;
sandbox.stub(Discourse.User, 'currentProp').withArgs('admin').returns(true);
},
teardown: function() {
Discourse.SiteSettings.min_topic_title_length = 15;
Discourse.SiteSettings.max_topic_title_length = 255;
Discourse.User.currentProp.restore();
}
equal(newComposer().get('originalText'), quote, "originalText is the quote" );
equal(newComposer().get('replyDirty'), false, "replyDirty is initally false with a quote" );
});
test("Title length for static page topics as admin", function() {
var composer = Discourse.Composer.create();
Discourse.SiteSettings.min_topic_title_length = 5;
Discourse.SiteSettings.max_topic_title_length = 10;
const composer = createComposer();
var post = Discourse.Post.create({id: 123, post_number: 2, static_doc: true});
const post = Discourse.Post.create({id: 123, post_number: 2, static_doc: true});
composer.setProperties({post: post, action: Discourse.Composer.EDIT });
composer.set('title', 'asdf');
+5 -1
View File
@@ -59,7 +59,11 @@ sinon.config = {
useFakeServer: false
};
window.assetPath = function() { return null; };
window.assetPath = function(url) {
if (url.indexOf('defer') === 0) {
return "/assets/" + url;
}
};
// Stop the message bus so we don't get ajax calls
window.MessageBus.stop();