diff --git a/Gruntfile.js b/Gruntfile.js
index 1ab9151cf9..f44b2f07fe 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -817,7 +817,7 @@ var path = require('path'),
grunt.registerTask('init', ['shell:bourbon', 'default']);
// Run unit tests
- grunt.registerTask('test-unit', ['setTestEnv', 'loadConfig', 'mochacli:all']);
+ grunt.registerTask('test-unit', ['setTestEnv', 'loadConfig', 'express:test', 'mochacli:all']);
// Run casperjs tests only
grunt.registerTask('test-functional', ['setTestEnv', 'express:test', 'spawn-casperjs']);
diff --git a/core/test/functional/admin/04_content_test.js b/core/test/functional/admin/content_test.js
similarity index 58%
rename from core/test/functional/admin/04_content_test.js
rename to core/test/functional/admin/content_test.js
index cd3174c9b9..485ab2a02a 100644
--- a/core/test/functional/admin/04_content_test.js
+++ b/core/test/functional/admin/content_test.js
@@ -1,12 +1,31 @@
/*globals casper, __utils__, url, testPost */
-casper.test.begin("Content screen is correct", 17, function suite(test) {
- test.filename = "content_test.png";
+CasperTest.begin("Content screen is correct", 20, function suite(test) {
+ // Create a sample post
+ casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
+ test.assertTitle('Ghost Admin', 'Ghost admin has no title');
+ });
- casper.start(url + "ghost/content/", function testTitleAndUrl() {
+ casper.then(function createTestPost() {
+ casper.sendKeys('#entry-title', testPost.title);
+ casper.writeContentToCodeMirror(testPost.html);
+ });
+
+ casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
+ test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
+ });
+
+ casper.thenClick('.js-publish-button');
+
+ casper.waitForResource(/posts/, function checkPostWasCreated() {
+ test.assertExists('.notification-success', 'got success notification');
+ });
+
+ // Begin test
+ casper.thenOpen(url + "ghost/content/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
- }).viewport(1280, 1024);
+ });
casper.then(function testMenus() {
test.assertExists("#main-menu", "Main menu is present");
@@ -25,40 +44,25 @@ casper.test.begin("Content screen is correct", 17, function suite(test) {
test.assertExists(".content-list-content", "Content list view is present");
test.assertExists(".content-list-content li .entry-title", "Content list view has at least one item");
test.assertExists(".content-preview", "Content preview is present");
- test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "first item is the post we created");
+ test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "item is present and has content");
});
casper.then(function testActiveItem() {
- casper.test.assertEvalEquals(function () {
+ test.assertEvalEquals(function () {
return document.querySelector('.content-list-content li').className;
}, "active", "first item is active");
}).thenClick(".content-list-content li:nth-child(2) a", function then() {
- casper.test.assertEvalEquals(function () {
+ test.assertEvalEquals(function () {
return document.querySelectorAll('.content-list-content li')[1].className;
}, "active", "second item is active");
});
-
- // TODO: finish testing delete
-// casper.then(function testDeletePost() {
-// casper.clickLabel(testPost.title, "h3");
-// });
-
- casper.run(function () {
- test.done();
- });
});
-casper.test.begin('Infinite scrolling', 1, function suite(test) {
- test.filename = 'content_infinite_scrolling_test.png';
-
+CasperTest.begin('Infinite scrolling', 1, function suite(test) {
// Placeholder for infinite scrolling/pagination tests (will need to setup 16+ posts).
- casper.start(url + "ghost/content/", function testTitleAndUrl() {
- test.assertTitle("Ghost Admin", "Ghost admin has no title");
- }).viewport(1280, 1024);
-
- casper.run(function () {
- test.done();
+ casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
+ test.assertTitle('Ghost Admin', 'Ghost admin has no title');
});
});
\ No newline at end of file
diff --git a/core/test/functional/admin/03_editor_test.js b/core/test/functional/admin/editor_test.js
similarity index 75%
rename from core/test/functional/admin/03_editor_test.js
rename to core/test/functional/admin/editor_test.js
index 57367e7f80..9bcbdf07b4 100644
--- a/core/test/functional/admin/03_editor_test.js
+++ b/core/test/functional/admin/editor_test.js
@@ -1,15 +1,12 @@
/*globals casper, __utils__, url, testPost */
-casper.test.begin("Ghost editor is correct", 9, function suite(test) {
- test.filename = "editor_test.png";
-
- casper.start(url + "ghost/editor/", function testTitleAndUrl() {
+CasperTest.begin("Ghost editor is correct", 10, function suite(test) {
+ casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
test.assertExists(".entry-markdown", "Ghost editor is present");
test.assertExists(".entry-preview", "Ghost preview is present");
- }).viewport(1280, 1024);
-
+ });
// test saving with no data
casper.thenClick('.js-publish-button');
@@ -26,9 +23,8 @@ casper.test.begin("Ghost editor is correct", 9, function suite(test) {
casper.writeContentToCodeMirror(testPost.html);
});
- // We must wait after sending keys to CodeMirror
- casper.wait(1000, function doneWait() {
- this.echo("I've waited for 1 seconds.");
+ casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
+ test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct.');
});
casper.thenClick('.js-publish-button');
@@ -40,54 +36,31 @@ casper.test.begin("Ghost editor is correct", 9, function suite(test) {
test.assertEvalEquals(function () {
return document.querySelector('#entry-title').value;
}, testPost.title, 'Title is correct');
-
- // TODO: make this work - spaces & newlines are problematic
- // test.assertTextExists(testPost.html, 'Post html exists');
- });
-
- casper.run(function () {
- test.done();
});
});
-
-casper.test.begin("Haunted markdown in editor works", 3, function suite(test) {
- test.filename = "markdown_test.png";
-
- casper.start(url + "ghost/editor/", function testTitleAndUrl() {
+CasperTest.begin("Haunted markdown in editor works", 3, function suite(test) {
+ casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
- }).viewport(1280, 1024);
+ });
casper.then(function testImage() {
casper.writeContentToCodeMirror("![sometext]()");
});
- // We must wait after sending keys to CodeMirror
- casper.wait(1000, function doneWait() {
- this.echo("I've waited for 1 seconds.");
- // bind to resource events so we can get the API response
- });
-
- casper.then(function checkPostWasCreated() {
-
+ casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
test.assertEvalEquals(function () {
return document.querySelector('.CodeMirror-wrap textarea').value;
- }, "![sometext]()", 'Editor value is correct');
+ }, '![sometext]()', 'Editor value is correct');
test.assertSelectorHasText('.entry-preview .rendered-markdown', 'Add image of sometext', 'Editor value is correct');
});
-
- casper.run(function () {
- test.done();
- });
});
-casper.test.begin("Word count and plurality", 4, function suite(test) {
- test.filename = "editor_plurality_test.png";
-
- casper.start(url + "ghost/editor/", function testTitleAndUrl() {
+CasperTest.begin("Word count and plurality", 4, function suite(test) {
+ casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
- }).viewport(1280, 1024);
+ });
casper.then(function checkZeroPlural() {
test.assertSelectorHasText('.entry-word-count', '0 words', 'count of 0 produces plural "words".');
@@ -97,42 +70,26 @@ casper.test.begin("Word count and plurality", 4, function suite(test) {
casper.writeContentToCodeMirror('test');
});
- // We must wait after sending keys to CodeMirror
- casper.wait(1000, function doneWait() {
- this.echo('I\'ve waited for 1 seconds.');
- });
-
- casper.then(function checkSinglular() {
+ casper.waitForSelectorTextChange('.entry-word-count', function onSuccess() {
test.assertSelectorHasText('.entry-word-count', '1 word', 'count of 1 produces singular "word".');
- });
+ })
casper.then(function () {
casper.writeContentToCodeMirror('test'); // append another word, assumes newline
});
- // We must wait after sending keys to CodeMirror
- casper.wait(1000, function doneWait() {
- this.echo('I\'ve waited for 1 seconds.');
- });
-
- casper.then(function checkPlural() {
+ casper.waitForSelectorTextChange('.entry-word-count', function onSuccess() {
test.assertSelectorHasText('.entry-word-count', '2 words', 'count of 2 produces plural "words".');
});
-
- casper.run(function () {
- test.done();
- });
});
-casper.test.begin('Title Trimming', function suite(test) {
+CasperTest.begin('Title Trimming', 2, function suite(test) {
var untrimmedTitle = ' test title ',
trimmedTitle = 'test title';
- test.filename = 'editor_title_trimming_test.png';
-
- casper.start(url + 'ghost/editor/', function testTitleAndUrl() {
+ casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');
- }).viewport(1280, 1024);
+ });
casper.then(function populateTitle() {
casper.sendKeys('#entry-title', untrimmedTitle);
@@ -143,18 +100,12 @@ casper.test.begin('Title Trimming', function suite(test) {
}, trimmedTitle, 'Entry title should match expected value.');
});
-
- casper.run(function () {
- test.done();
- });
});
-casper.test.begin('Publish menu - new post', function suite(test) {
- test.filename = 'publish_menu_new_post.png';
-
- casper.start(url + 'ghost/editor/', function testTitleAndUrl() {
+CasperTest.begin('Publish menu - new post', 11, function suite(test) {
+ casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');
- }).viewport(1280, 1024);
+ });
// ... check default option status, label, class
casper.then(function () {
@@ -184,28 +135,21 @@ casper.test.begin('Publish menu - new post', function suite(test) {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'published');
}, 'Publish button\'s updated status should be "published"');
});
-
- casper.run(function () {
- test.done();
- });
});
-casper.test.begin('Publish menu - existing post', function suite(test) {
- test.filename = 'publish_menu_existing_post.png';
-
+CasperTest.begin('Publish menu - existing post', 24, function suite(test) {
// Create a post, save it and test refreshed editor
- casper.start(url + 'ghost/editor/', function testTitleAndUrl() {
+ casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');
- }).viewport(1280, 1024);
+ });
casper.then(function createTestPost() {
casper.sendKeys('#entry-title', testPost.title);
casper.writeContentToCodeMirror(testPost.html);
});
- // We must wait after sending keys to CodeMirror
- casper.wait(1000, function doneWait() {
- this.echo("I've waited for 1 seconds.");
+ casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
+ test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
// Create a post in draft status
@@ -279,8 +223,4 @@ casper.test.begin('Publish menu - existing post', function suite(test) {
return (__utils__.findOne('.js-publish-button').getAttribute('data-status') === 'draft');
}, 'Publish button\'s updated status should be "draft"');
});
-
- casper.run(function () {
- test.done();
- });
});
\ No newline at end of file
diff --git a/core/test/functional/admin/06_flow_test.js b/core/test/functional/admin/flow_test.js
similarity index 67%
rename from core/test/functional/admin/06_flow_test.js
rename to core/test/functional/admin/flow_test.js
index b49a89d274..2162632e72 100644
--- a/core/test/functional/admin/06_flow_test.js
+++ b/core/test/functional/admin/flow_test.js
@@ -3,22 +3,18 @@
*/
/*globals casper, __utils__, url, testPost */
-casper.test.begin("Ghost edit draft flow works correctly", 7, function suite(test) {
- test.filename = "flow_test.png";
-
- casper.start(url + "ghost/editor/", function then() {
+CasperTest.begin("Ghost edit draft flow works correctly", 8, function suite(test) {
+ casper.thenOpen(url + "ghost/editor/", function then() {
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
- }).viewport(1280, 1024);
-
- // First, create a new draft post
- casper.then(function createTestPost() {
- casper.sendKeys('#entry-title', 'Test Draft Post');
- casper.writeContentToCodeMirror('I am a draft');
});
- // We must wait after sending keys to CodeMirror
- casper.wait(1000, function doneWait() {
- this.echo("I've waited for 1 seconds.");
+ casper.then(function createTestPost() {
+ casper.sendKeys('#entry-title', testPost.title);
+ casper.writeContentToCodeMirror(testPost.html);
+ });
+
+ casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() {
+ test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct');
});
casper.thenClick('.js-publish-button');
@@ -39,7 +35,7 @@ casper.test.begin("Ghost edit draft flow works correctly", 7, function suite(tes
return document.querySelector('.content-list-content li').className;
}, "active", "first item is active");
- test.assertSelectorHasText(".content-list-content li:first-child h3", 'Test Draft Post', "first item is the post we created");
+ test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "first item is the post we created");
});
casper.thenClick('.post-edit').waitForResource(/editor/, function then() {
@@ -54,16 +50,11 @@ casper.test.begin("Ghost edit draft flow works correctly", 7, function suite(tes
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
-
- casper.run(function () {
- test.done();
- });
});
// TODO: test publishing, editing, republishing, unpublishing etc
-//casper.test.begin("Ghost edit published flow works correctly", 6, function suite(test) {
+//CasperTest.begin("Ghost edit published flow works correctly", 6, function suite(test) {
//
-// test.filename = "flow_test.png";
//
//
//});
\ No newline at end of file
diff --git a/core/test/functional/admin/01_login_test.js b/core/test/functional/admin/login_test.js
similarity index 59%
rename from core/test/functional/admin/01_login_test.js
rename to core/test/functional/admin/login_test.js
index 9d3a5b7d93..a150da3880 100644
--- a/core/test/functional/admin/01_login_test.js
+++ b/core/test/functional/admin/login_test.js
@@ -1,21 +1,13 @@
/*globals casper, __utils__, url, user, falseUser */
-casper.test.begin('Ensure Session is Killed', 1, function suite(test) {
- test.filename = 'login_logout_test.png';
-
- casper.start(url + 'logout/', function (response) {
+CasperTest.begin('Ensure Session is Killed', 1, function suite(test) {
+ casper.thenOpen(url + 'logout/', function (response) {
test.assertUrlMatch(/ghost\/sign/, 'We got redirected to signin or signup page');
});
+}, true);
- casper.run(function () {
- test.done();
- });
-});
-
-casper.test.begin('Ensure a User is Registered', 2, function suite(test) {
- test.filename = 'login_user_registered_test.png';
-
- casper.start(url + 'ghost/signup/').viewport(1280, 1024);
+CasperTest.begin('Ensure a User is Registered', 2, function suite(test) {
+ casper.thenOpen(url + 'ghost/signup/');
casper.waitForOpaque(".signup-box",
function then() {
@@ -28,61 +20,35 @@ casper.test.begin('Ensure a User is Registered', 2, function suite(test) {
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'already registered');
// If the previous assert succeeds, then we should skip the next check and just pass.
- test.pass('Already registered!');
+ casper.echo('Already registered!');
}, function onTimeout() {
test.assertUrlMatch(/\/ghost\/$/, 'If we\'re not already registered, we should be logged in.');
- test.pass('Successfully registered.');
+ casper.echo('Successfully registered.');
}, 2000);
- casper.run(function () {
- test.done();
+ casper.thenOpen(url + 'logout/', function then() {
+ test.assertUrlMatch(/ghost\/signin/, 'We got redirected to signin page.');
});
-});
+}, true);
-casper.test.begin('Ensure Session is Killed after Registration', 1, function suite(test) {
- test.filename = 'login_logout2_test.png';
-
- casper.start(url + 'logout/', function then() {
- test.assertUrlMatch(/ghost\/signin/, 'We got redirected to signin page');
- });
-
- casper.run(function () {
- test.done();
- });
-});
-
-casper.test.begin("Ghost admin will load login page", 2, function suite(test) {
- test.filename = "admin_test.png";
-
- casper.start(url + "ghost", function testTitleAndUrl() {
+CasperTest.begin("Ghost admin will load login page", 2, function suite(test) {
+ casper.thenOpen(url + "ghost", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
- test.assertUrlMatch(/ghost\/signin\/$/, 'If we\'re not already registered, we should be logged in.');
- }).viewport(1280, 1024);
-
- casper.run(function () {
- test.done();
+ test.assertUrlMatch(/ghost\/signin\/$/, 'We should be presented with the signin page.');
});
-});
-
-casper.test.begin('Redirects to signin', 2, function suite(test) {
- test.filename = 'login_redirect_test.png';
+}, true);
+CasperTest.begin('Redirects login to signin', 2, function suite(test) {
casper.start(url + 'ghost/login/', function testRedirect(response) {
test.assertEqual(response.status, 200, 'Response status should be 200.');
test.assertUrlMatch(/ghost\/signin\/$/, 'Should be redirected to /signin/.');
});
+}, true);
- casper.run(function () {
- test.done();
- });
-});
-
-casper.test.begin("Can't spam it", 4, function suite(test) {
- test.filename = "login_spam_test.png";
-
- casper.start(url + "ghost/signin/", function testTitle() {
+CasperTest.begin("Can't spam it", 4, function suite(test) {
+ casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
- }).viewport(1280, 1024);
+ });
casper.waitForOpaque(".login-box",
function then() {
@@ -107,18 +73,12 @@ casper.test.begin("Can't spam it", 4, function suite(test) {
// This test causes the spam notification
// add a wait to ensure future tests don't get tripped up by this.
casper.wait(1000);
+}, true);
- casper.run(function () {
- test.done();
- });
-});
-
-casper.test.begin("Can login to Ghost", 4, function suite(test) {
- test.filename = "login_test.png";
-
- casper.start(url + "ghost/login/", function testTitle() {
+CasperTest.begin("Can login to Ghost", 4, function suite(test) {
+ casper.thenOpen(url + "ghost/login/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
- }).viewport(1280, 1024);
+ });
casper.waitForOpaque(".login-box",
function then() {
@@ -132,8 +92,4 @@ casper.test.begin("Can login to Ghost", 4, function suite(test) {
}, function onTimeOut() {
test.fail('Failed to load ghost/ resource');
});
-
- casper.run(function () {
- test.done();
- });
-});
+}, true);
diff --git a/core/test/functional/admin/07_logout_test.js b/core/test/functional/admin/logout_test.js
similarity index 80%
rename from core/test/functional/admin/07_logout_test.js
rename to core/test/functional/admin/logout_test.js
index 5e805ad522..692d8b0337 100644
--- a/core/test/functional/admin/07_logout_test.js
+++ b/core/test/functional/admin/logout_test.js
@@ -3,12 +3,12 @@
*/
/*globals casper, __utils__, url, testPost, falseUser, email */
-casper.test.begin("Ghost logout works correctly", 2, function suite(test) {
- test.filename = "logout_test.png";
+CasperTest.begin("Ghost logout works correctly", 2, function suite(test) {
+ CasperTest.Routines.login.run(test);
- casper.start(url + "ghost/", function then() {
+ casper.thenOpen(url + "ghost/", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/", "Ghost doesn't require login this time");
- }).viewport(1280, 1024);
+ });
casper.thenClick('#usermenu a').waitFor(function checkOpaque() {
return this.evaluate(function () {
@@ -26,19 +26,13 @@ casper.test.begin("Ghost logout works correctly", 2, function suite(test) {
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
-
- casper.run(function () {
- test.done();
- });
-});
+}, true);
// has to be done after signing out
-casper.test.begin("Can't spam signin", 3, function suite(test) {
- test.filename = "spam_test.png";
-
- casper.start(url + "ghost/signin/", function testTitle() {
+CasperTest.begin("Can't spam signin", 3, function suite(test) {
+ casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
- }).viewport(1280, 1024);
+ });
casper.waitFor(function checkOpaque() {
return this.evaluate(function () {
@@ -60,18 +54,12 @@ casper.test.begin("Can't spam signin", 3, function suite(test) {
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
+}, true);
- casper.run(function () {
- test.done();
- });
-});
-
-casper.test.begin("Ghost signup fails properly", 5, function suite(test) {
- test.filename = "signup_test.png";
-
- casper.start(url + "ghost/signup/", function then() {
+CasperTest.begin("Ghost signup fails properly", 5, function suite(test) {
+ casper.thenOpen(url + "ghost/signup/", function then() {
test.assertEquals(casper.getCurrentUrl(), url + "ghost/signup/", "Reached signup page");
- }).viewport(1280, 1024);
+ });
casper.then(function signupWithShortPassword() {
this.fill("#signup", {email: email, password: 'test'}, true);
@@ -98,8 +86,4 @@ casper.test.begin("Ghost signup fails properly", 5, function suite(test) {
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
-
- casper.run(function () {
- test.done();
- });
-});
\ No newline at end of file
+}, true);
\ No newline at end of file
diff --git a/core/test/functional/admin/05_settings_test.js b/core/test/functional/admin/settings_test.js
similarity index 86%
rename from core/test/functional/admin/05_settings_test.js
rename to core/test/functional/admin/settings_test.js
index 796cdf63e5..0882aef54a 100644
--- a/core/test/functional/admin/05_settings_test.js
+++ b/core/test/functional/admin/settings_test.js
@@ -1,12 +1,10 @@
/*globals casper, __utils__, url */
-casper.test.begin("Settings screen is correct", 15, function suite(test) {
- test.filename = "settings_test.png";
-
- casper.start(url + "ghost/settings/", function testTitleAndUrl() {
+CasperTest.begin("Settings screen is correct", 15, function suite(test) {
+ casper.thenOpen(url + "ghost/settings/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
- }).viewport(1280, 1024);
+ });
casper.then(function testViews() {
test.assertExists(".wrapper", "Settings main view is present");
@@ -41,14 +39,14 @@ casper.test.begin("Settings screen is correct", 15, function suite(test) {
function handleUserRequest(requestData, request) {
// make sure we only get requests from the user pane
if (requestData.url.indexOf('settings/') !== -1) {
- casper.test.fail("Saving the user pane triggered another settings pane to save");
+ test.fail("Saving the user pane triggered another settings pane to save");
}
}
function handleSettingsRequest(requestData, request) {
// make sure we only get requests from the user pane
if (requestData.url.indexOf('users/') !== -1) {
- casper.test.fail("Saving a settings pane triggered the user pane to save");
+ test.fail("Saving a settings pane triggered the user pane to save");
}
}
@@ -64,7 +62,7 @@ casper.test.begin("Settings screen is correct", 15, function suite(test) {
}, function doneWaiting() {
}, function waitTimeout() {
- casper.test.fail("Saving the user pane did not result in a notification");
+ test.fail("Saving the user pane did not result in a notification");
});
casper.then(function checkUserWasSaved() {
@@ -92,7 +90,7 @@ casper.test.begin("Settings screen is correct", 15, function suite(test) {
}, function doneWaiting() {
}, function waitTimeout() {
- casper.test.fail("Saving the general pane did not result in a notification");
+ test.fail("Saving the general pane did not result in a notification");
});
casper.then(function checkSettingsWereSaved() {
@@ -105,22 +103,19 @@ casper.test.begin("Settings screen is correct", 15, function suite(test) {
test.assert(false, 'No success notification :(');
});
- casper.run(function () {
+ CasperTest.beforeDone(function () {
casper.removeListener('resource.requested', handleUserRequest);
casper.removeListener('resource.requested', handleSettingsRequest);
- test.done();
});
});
-casper.test.begin("User settings screen validates email", 6, function suite(test) {
+CasperTest.begin("User settings screen validates email", 6, function suite(test) {
var email, brokenEmail;
- test.filename = "user_settings_test.png";
-
- casper.start(url + "ghost/settings/user/", function testTitleAndUrl() {
+ casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time");
- }).viewport(1280, 1024);
+ });
casper.then(function setEmailToInvalid() {
email = casper.getElementInfo('#user-email').attributes.value;
@@ -158,8 +153,4 @@ casper.test.begin("User settings screen validates email", 6, function suite(test
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
-
- casper.run(function () {
- test.done();
- });
});
\ No newline at end of file
diff --git a/core/test/functional/base.js b/core/test/functional/base.js
index 6a193ee362..8c371cb5f6 100644
--- a/core/test/functional/base.js
+++ b/core/test/functional/base.js
@@ -83,4 +83,141 @@ casper.on("page.error", function (msg, trace) {
casper.test.on("fail", function captureFailure() {
var filename = casper.test.filename || "casper_test_fail.png";
casper.capture(new Date().getTime() + '_' + filename);
-});
\ No newline at end of file
+});
+
+var CasperTest = (function() {
+
+ var _beforeDoneHandler,
+ _noop = function noop() { },
+ _isUserRegistered = false;
+
+ // Always log out at end of test.
+ casper.test.tearDown(function (done) {
+ casper.then(_beforeDoneHandler);
+
+ casper.thenOpen(url + 'signout/');
+
+ casper.waitForResource(/ghost\/sign/);
+
+ casper.run(done);
+ });
+
+ // Wrapper around `casper.test.begin`
+ function begin(testName, expect, suite, doNotAutoLogin) {
+ _beforeDoneHandler = _noop;
+
+ var runTest = function (test) {
+ test.filename = testName.toLowerCase().replace(/ /g, '-').concat('.png');
+
+ casper.start().viewport(1280, 1024);
+
+ if (!doNotAutoLogin) {
+ // Only call register once for the lifetime of Mindless
+ if (!_isUserRegistered) {
+ CasperTest.Routines.logout.run(test);
+ CasperTest.Routines.register.run(test);
+
+ _isUserRegistered = true;
+ }
+
+ /* Ensure we're logged out at the start of every test or we may get
+ unexpected failures. */
+ CasperTest.Routines.logout.run(test);
+ CasperTest.Routines.login.run(test);
+ }
+
+ suite.call(casper, test);
+
+ casper.run(function () {
+ test.done();
+ });
+ };
+
+ if (typeof expect === 'function') {
+ doNotAutoLogin = suite;
+ suite = expect;
+
+ casper.test.begin(testName, runTest);
+ }
+ else {
+ casper.test.begin(testName, expect, runTest);
+ }
+ }
+
+ // Sets a handler to be invoked right before `test.done` is invoked
+ function beforeDone(fn) {
+ if (fn) {
+ _beforeDoneHandler = fn;
+ }
+ else {
+ _beforeDoneHandler = _noop;
+ }
+ }
+
+ return {
+ begin: begin,
+ beforeDone: beforeDone
+ };
+
+}());
+
+CasperTest.Routines = (function () {
+
+ function register(test) {
+ casper.thenOpen(url + 'ghost/signup/').viewport(1280, 1024);
+
+ casper.waitForOpaque('.signup-box', function then() {
+ this.fill('#signup', newUser, true);
+ });
+
+ casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
+ this.echo('It appears as though a user is already registered.');
+ }, function onTimeout() {
+ this.echo('It appears as though a user was not already registered.');
+ }, 2000);
+ }
+
+ function login(test) {
+ casper.thenOpen(url + 'ghost/signin/');
+
+ casper.waitForOpaque('.login-box', function then() {
+ this.fill("#login", user, true);
+ });
+
+ casper.waitForResource(/ghost\/$/);
+ }
+
+ function logout(test) {
+ casper.thenOpen(url + 'signout/');
+ // Wait for signin or signup
+ casper.waitForResource(/ghost\/sign/);
+ }
+
+ function deletePost(id) {
+ casper.thenOpen(url + 'ghost/');
+
+ casper.thenEvaluate(function (id) {
+ return __utils__.sendAJAX(Ghost.settings.apiRoot + '/posts/' + id, 'DELETE');
+ }, id);
+ }
+
+ function _createRunner(fn) {
+ fn.run = function run(test) {
+ var routine = this;
+
+ casper.then(function () {
+ routine.call(casper, test);
+ });
+ };
+
+ return fn;
+ }
+
+ return {
+ register: _createRunner(register),
+ login: _createRunner(login),
+ logout: _createRunner(logout),
+ deletePost: _createRunner(deletePost)
+ };
+
+}());
\ No newline at end of file
diff --git a/core/test/functional/frontend/01_route_test.js b/core/test/functional/frontend/01_route_test.js
deleted file mode 100644
index a10c4a23e6..0000000000
--- a/core/test/functional/frontend/01_route_test.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Tests logging out and attempting to sign up
- */
-
-/*globals casper, __utils__, url, testPost, falseUser, email */
-casper.test.begin('Redirects page 1 request', 1, function suite(test) {
- test.filename = '01_route_test_redirects_page_1.png';
-
- casper.start(url + 'page/1/', function then(response) {
- test.assertEqual(casper.getCurrentUrl().indexOf('page/'), -1, 'Should be redirected to "/".');
- }).viewport(1280, 1024);
-
- casper.run(function () {
- test.done();
- });
-});
\ No newline at end of file
diff --git a/core/test/functional/frontend/route_test.js b/core/test/functional/frontend/route_test.js
new file mode 100644
index 0000000000..67da4867c9
--- /dev/null
+++ b/core/test/functional/frontend/route_test.js
@@ -0,0 +1,10 @@
+/**
+ * Tests logging out and attempting to sign up
+ */
+
+/*globals casper, __utils__, url, testPost, falseUser, email */
+CasperTest.begin('Redirects page 1 request', 1, function suite(test) {
+ casper.thenOpen(url + 'page/1/', function then(response) {
+ test.assertEqual(casper.getCurrentUrl().indexOf('page/'), -1, 'Should be redirected to "/".');
+ });
+}, true);
\ No newline at end of file
diff --git a/core/test/functional/frontend/02_rss_test.js b/core/test/functional/frontend/rss_test.js
similarity index 58%
rename from core/test/functional/frontend/02_rss_test.js
rename to core/test/functional/frontend/rss_test.js
index fbc2d8c491..e27e94c112 100644
--- a/core/test/functional/frontend/02_rss_test.js
+++ b/core/test/functional/frontend/rss_test.js
@@ -1,16 +1,10 @@
/**
* Tests if RSS exists and is working
*/
-casper.test.begin('Ensure that RSS is available', 3, function suite(test) {
- test.filename = 'rss_test.png';
-
- casper.start(url + 'rss/', function (response) {
+CasperTest.begin('Ensure that RSS is available', 3, function suite(test) {
+ casper.thenOpen(url + 'rss/', function (response) {
test.assertEqual(response.status, 200, 'Response status should be 200.');
test.assert(this.getPageContent().indexOf(' ' + newPost.markdown + ' Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est. mctesters Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.HTML Ipsum Presents
commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.Header Level 2
Header Level 3
"
+ },
+ {
+ title: "Kitchen Sink",
+ slug: "kitchen-sink",
+ markdown: "#header h1 a{display: block;width: 300px;height: 80px;}
HTML Ipsum Presents
commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.Header Level 2
Header Level 3
",
+ },
+ {
+ title: "Short and Sweet",
+ slug: "short-and-sweet",
+ markdown: "## testing\n\nmctesters\n\n- test\n- line\n- items",
+ html: "#header h1 a{display: block;width: 300px;height: 80px;}
testing
\n\n\n
"
+ },
+ {
+ title: "Not so short, bit complex",
+ slug: "not-so-short-bit-complex",
+ markdown: "1 2 3 4 a b c d e f g h i j k l
Three days ago I released a concept page<\/a> for a lite version of WordPress that I've been thinking about for a long time, called Ghost. I think it's fair to say that I didn't quite anticipate how strong the reaction would be - and I've hardly had time to catch my breath in the last 72 hours.<\/p>\n The response was overwhelming, and overwhelmingly positive. In the first 6 hours my site got 35,000 page views after hitting the number 1 slot on Hacker News<\/a>. As of right now, the traffic count is just over 91,000 page views<\/a> - and Ghost has been featured all over the place. Notable mentions so far include Christina Warren from Mashable, who wrote about it<\/a>. Michael Carney from PandoDaily interviewed me about it<\/a>. Someone even wrote about it in Chinese<\/a>. That's pretty cool.\\n The feedback has been amazing, and while it's impossible to reply to all of the messages individually, I'm getting to as many of them as I can and I want to thank each and every one of you who took the time to send me a message or share the concept because you liked it. Now that the initial storm has died down a bit, I wanted to take some time to answer some of the more common questions and talk about what's next.<\/p>\n The most common question, bizarrely:<\/p>\n This was really the only negativity I got in response to the post, and it surprised me. I put together the concept page as... just that... a concept. It was a way for me to get the ideas out of my head and \"down on paper\" - or so to speak. I used photoshop as a tool<\/em> to write down my idea with text and images. If I used a sketchbook as a tool <\/em>to create images and handwritten notes, then uploaded scans of it, I doubt anyone would complain. The concept page was never supposed to be a finished product because I had no idea if there would be any interest in it. I had no motivation to waste hours coding a custom layout for something might only ever be read by a few people and then forgotten.<\/p>\n Hardware manufacturers make hundreds of foam cutout prototypes of products before they build one with working buttons and screens. I'm aware of all the usability problems with a web page made of images, and equally, foam cutouts without buttons or screens aren't particularly user friendly either. They're not supposed to be.<\/p>\n Let's move on.<\/p>\n Because comments add a layer of complexity that is beyond the core focus of this platform, which is publishing. Again, that's not to say you couldn't have any comments. This could easily be added with a dedicated plugin where you own the data or (as mentioned) there are third party providers such as Disqus, IntenseDebate, Livefyre and Facebook who all have great platforms. The point of this isn't to say \"you can't have comments\" - it's to say \"comments aren't on by default\". It's about simplicity, more than anything else.<\/p>\n Sorry, but Tumblr already did this - it's not the future of blogging, it's the past.<\/p>\n Ghost isn't about sharing \"Fuck Yeah [Dogs<\/a>\/Sharks<\/a>\/Girls with Tattoos<\/a>]\" - it's about publishing - which means writing - rather than mashing a few buttons to make sure that everyone can see and appreciate your latest funny picture\/status, which is surely the most funny picture\/status you've ever posted.<\/p>\n Tumblr, Pinterest and Facebook already have this locked down. It's not the future.<\/p>\n The concept page was a way for me to test demand and interest. To see if anyone actually agreed with my frustrations and, more importantly, my solutions. I plucked a random figure of \"10,000 pageviews\" out of the air before I hit the publish button. If it got less than 10,000 pageviews, I would surrender to the fact that it would only ever be an idea. I've now exceeded that goal 9 times over, so yes, I'm looking at how Ghost can now be made into a reality.<\/p>\n Ok, ok - there's a holding page up on http:\/\/TryGhost.org<\/a> - put your email address in.<\/p>\n There's three main ways of going about this, each has merits as well as drawbacks.<\/p>\n 1.) Build it from scratch<\/strong><\/em> - Many people (particularly the Hacker News crowd) expressed the sentiment that there was little point in forking WordPress. When you're going to strip out so much, you get to a point where you might as well start from scratch anyway. Take away the crutches of being stuck with older technologies and put together something which is as sophisticated in code as it is in UI\/UX.<\/p>\n 2.) Fork WordPress<\/strong><\/em> - This was the original idea I put out. Take the WordPress codebase, as is, and modify it to turn it into something new. Initially the codebase is practically the same, which means developers already know it. Then it can change over time and evolve into its own thing.<\/p>\n 3.) Make it a plugin\/extension<\/strong><\/em> - Lots of people asked why Ghost couldn't just be a WordPress plugin. It would certainly be the easiest route of the 3, it's possible to completely remove \/wp-admin\/ and replace with with \/ghost\/ ... but I feel like it kind of misses the point. This route bolts Ghost on, but it's still WordPress under the hood. From a UI\/UX standpoint it would function - but it wouldn't revolutionise anything else. It makes WordPress itself about blogging again, rather than creating something new.<\/p>\n I've spoken to a lot of smart people over the last few days. The one thing that everyone seems to agree on is that a fork is the worst of both worlds. So the one thing that I suggested as a way of making this happen, is the least likely to work in reality. Remember the foam prototype metaphor earlier? Learning and iterating - that's what happening now.<\/p>\n That leaves a choice between WordPress plugin or fresh build. The answer? Both.<\/p>\n A WordPress plugin will act as a proof of concept and a working prototype, initially, because it's easier to leverage the existing WordPress ecosystem to create it than to go into a cave for 6 months trying to build this amazing thing that everyone will have forgotten about.<\/p>\n The plugin will not be perfect. It will add the Ghost UI\/UX and as much functionality as we can cram into it. It will completely remove \/wp-admin\/ and replace it with \/ghost\/ - effectively using WordPress core as a basic foundation to build on top of. It will give people who don't want to switch away from WordPress access to the Ghost UX which they want to have, and it will give people who want the full Ghost platform a taste of what's to come.<\/p>\n It will allow us to develop and learn and iterate on the concept pretty rapidly, which has a great deal of value.<\/p>\n This is step one. Assuming the plugin is actually used by people - it would then justify exploring building the standalone version of Ghost from the ground up. The plugin would subsequently serve as a great marketing tool for the platform. Think of it as an upgrade path. But that's a long way away. Having the idea is the easy part. Making it happen is what counts.<\/p>\n Happily - amongst the thousands of people talking about Ghost for the last few days - several have been talking about how they've already built some working prototypes of my mockups and turned them into WordPress plugins or just local development sites. These will likely go on to be the starting point of the first Ghost plugin.<\/p>\n There's a lot to do, and I'm amazed by the number of people who have offered their help with this. In the next few days I'll be kicking off work on the plugin properly and start putting together a more organised structure which explains how you can get involved and contribute to the project if you're interested. So... watch this space - and thanks for all your support so far.<\/p>\n Follow @TryGhost<\/a><\/p>",
+ image: 'ghostpost.jpg',
+ status: status,
+ language: language
+ });
+ }
+
+ function createTag(overrides) {
+ return _.defaults(overrides, {
+ uuid: uuid.v4(),
+ updated_at: new Date(),
+ updated_by: 1,
+ created_at: new Date(),
+ created_by: 1
+ });
+ }
+
+ function createUser(overrides) {
+ return _.defaults(overrides, {
+ uuid: uuid.v4(),
+ created_by: 1,
+ created_at: new Date()
+ });
+ }
+
+ function createGenericUser(uniqueInteger) {
+ return createUser({
+ name: 'Joe Bloggs',
+ slug: 'joe-blogs',
+ email: 'joe_' + uniqueInteger + '@example.com',
+ password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6'
+ });
+ }
+
+ function createUserRole(userId, roleId) {
+ return {
+ role_id: roleId,
+ user_id: userId
+ };
+ }
+
+ posts = [
+ createPost(DataGenerator.Content.posts[0]),
+ createPost(DataGenerator.Content.posts[1]),
+ createPost(DataGenerator.Content.posts[2]),
+ createPost(DataGenerator.Content.posts[3])
+ ];
+
+ tags = [
+ createTag(DataGenerator.Content.tags[0]),
+ createTag(DataGenerator.Content.tags[1]),
+ createTag(DataGenerator.Content.tags[2]),
+ createTag(DataGenerator.Content.tags[3])
+ ];
+
+ posts_tags = [
+ { post_id: 2, tag_id: 2 },
+ { post_id: 2, tag_id: 3 },
+ { post_id: 3, tag_id: 3 },
+ { post_id: 4, tag_id: 4 },
+ { post_id: 5, tag_id: 5 }
+ ];
+
+ return {
+ createPost: createPost,
+ createGenericPost: createGenericPost,
+ createTag: createTag,
+ createUser: createUser,
+ createGenericUser: createGenericUser,
+ createUserRole: createUserRole,
+
+ posts: posts,
+ tags: tags,
+ posts_tags: posts_tags
+ };
+
+}());
+
+DataGenerator.forModel = (function () {
+
+ var posts,
+ tags,
+ users;
+
+ posts = _.map(DataGenerator.Content.posts, function (post) {
+ return _.pick(post, 'title', 'markdown');
+ });
+
+ tags = DataGenerator.Content.tags;
+
+ users = _.map(DataGenerator.Content.users, function (user) {
+ var user = _.pick(user, 'name', 'email');
+
+ return _.defaults({
+ password: 'Sl1m3rson'
+ }, user);
+ });
+
+ return {
+ posts: posts,
+ tags: tags,
+ users: users
+ };
+
+}());
+
+module.exports = DataGenerator;
\ No newline at end of file
diff --git a/core/test/unit/ghost_spec.js b/core/test/unit/ghost_spec.js
index 13cb58fa45..3283bbf99c 100644
--- a/core/test/unit/ghost_spec.js
+++ b/core/test/unit/ghost_spec.js
@@ -22,6 +22,7 @@ describe("Ghost API", function () {
});
beforeEach(function (done) {
+ this.timeout(5000);
sandbox = sinon.sandbox.create();
testUtils.initData().then(function () {
diff --git a/core/test/unit/api_permissions_spec.js b/core/test/unit/model_permissions_spec.js
similarity index 53%
rename from core/test/unit/api_permissions_spec.js
rename to core/test/unit/model_permissions_spec.js
index da9628f284..469537dcac 100644
--- a/core/test/unit/api_permissions_spec.js
+++ b/core/test/unit/model_permissions_spec.js
@@ -6,100 +6,6 @@ var testUtils = require('./testUtils'),
// Stuff we are testing
Models = require('../../server/models');
-describe("Role Model", function () {
-
- var RoleModel = Models.Role;
-
- should.exist(RoleModel);
-
- before(function (done) {
- testUtils.clearData().then(function () {
- done();
- }, done);
- });
-
- beforeEach(function (done) {
- this.timeout(5000);
- testUtils.initData().then(function () {
- done();
- }, done);
- });
-
- afterEach(function (done) {
- testUtils.clearData().then(function () {
- done();
- }, done);
- });
-
- it("can browse roles", function (done) {
- RoleModel.browse().then(function (foundRoles) {
- should.exist(foundRoles);
-
- foundRoles.models.length.should.be.above(0);
-
- done();
- }).then(null, done);
- });
-
- it("can read roles", function (done) {
- RoleModel.read({id: 1}).then(function (foundRole) {
- should.exist(foundRole);
-
- done();
- }).then(null, done);
- });
-
- it("can edit roles", function (done) {
- RoleModel.read({id: 1}).then(function (foundRole) {
- should.exist(foundRole);
-
- return foundRole.set({name: "updated"}).save();
- }).then(function () {
- return RoleModel.read({id: 1});
- }).then(function (updatedRole) {
- should.exist(updatedRole);
-
- updatedRole.get("name").should.equal("updated");
-
- done();
- }).then(null, done);
- });
-
- it("can add roles", function (done) {
- var newRole = {
- name: "test1",
- description: "test1 description"
- };
-
- RoleModel.add(newRole).then(function (createdRole) {
- should.exist(createdRole);
-
- createdRole.attributes.name.should.equal(newRole.name);
- createdRole.attributes.description.should.equal(newRole.description);
-
- done();
- }).then(null, done);
- });
-
- it("can delete roles", function (done) {
- RoleModel.read({id: 1}).then(function (foundRole) {
- should.exist(foundRole);
-
- return RoleModel['delete'](1);
- }).then(function () {
- return RoleModel.browse();
- }).then(function (foundRoles) {
- var hasRemovedId = foundRoles.any(function (role) {
- return role.id === 1;
- });
-
- hasRemovedId.should.equal(false);
-
- done();
- }).then(null, done);
- });
-});
-
describe("Permission Model", function () {
var PermissionModel = Models.Permission;
diff --git a/core/test/unit/model_posts_spec.js b/core/test/unit/model_posts_spec.js
new file mode 100644
index 0000000000..7e16d0694a
--- /dev/null
+++ b/core/test/unit/model_posts_spec.js
@@ -0,0 +1,364 @@
+/*globals describe, before, beforeEach, afterEach, it */
+var testUtils = require('./testUtils'),
+ should = require('should'),
+ _ = require('underscore'),
+ when = require('when'),
+ sequence = require('when/sequence'),
+
+ // Stuff we are testing
+ Models = require('../../server/models');
+
+describe('Post Model', function () {
+
+ var PostModel = Models.Post,
+ UserModel = Models.User;
+
+ before(function (done) {
+ testUtils.clearData().then(function () {
+ done();
+ }, done);
+ });
+
+ beforeEach(function (done) {
+ this.timeout(5000);
+ testUtils.initData()
+ .then(function () {
+ return testUtils.insertDefaultFixtures();
+ })
+ .then(function () {
+ done();
+ }, done);
+ });
+
+ afterEach(function (done) {
+ testUtils.clearData().then(function () {
+ done();
+ }, done);
+ });
+
+ it('can browse', function (done) {
+ PostModel.browse().then(function (results) {
+ should.exist(results);
+ results.length.should.be.above(1);
+
+ // should be in published_at, DESC order
+ // model and API differ here - need to fix
+ //results.models[0].attributes.published_at.should.be.above(results.models[1].attributes.published_at);
+
+ done();
+ }).then(null, done);
+ });
+
+ it('can read', function (done) {
+ var firstPost;
+
+ PostModel.browse().then(function (results) {
+ should.exist(results);
+ results.length.should.be.above(0);
+ firstPost = results.models[0];
+
+ return PostModel.read({slug: firstPost.attributes.slug});
+ }).then(function (found) {
+ should.exist(found);
+ found.attributes.title.should.equal(firstPost.attributes.title);
+
+ done();
+ }).then(null, done);
+ });
+
+ it('can findAll, returning author and user data', function (done) {
+ var firstPost;
+
+ PostModel.findAll({}).then(function (results) {
+ should.exist(results);
+ results.length.should.be.above(0);
+ firstPost = results.models[0].toJSON();
+
+ firstPost.author.should.be.a('object');
+ firstPost.user.should.be.a('object');
+ firstPost.author.name.should.equal(DataGenerator.Content.users[0].name);
+ firstPost.user.name.should.equal(DataGenerator.Content.users[0].name);
+
+ done();
+ }, done);
+ });
+
+ it('can findOne, returning author and user data', function (done) {
+ this.timeout(5000);
+ var firstPost;
+
+ PostModel.findOne({}).then(function (result) {
+ should.exist(result);
+ firstPost = result.toJSON();
+
+ firstPost.author.should.be.a('object');
+ firstPost.user.should.be.a('object');
+ firstPost.author.name.should.equal(testUtils.DataGenerator.Content.users[0].name);
+ firstPost.user.name.should.equal(testUtils.DataGenerator.Content.users[0].name);
+
+ done();
+ }, done);
+ });
+
+ it('can edit', function (done) {
+ var firstPost;
+
+ PostModel.browse().then(function (results) {
+ should.exist(results);
+ results.length.should.be.above(0);
+ firstPost = results.models[0];
+
+ return PostModel.edit({id: firstPost.id, title: 'new title'});
+ }).then(function (edited) {
+ should.exist(edited);
+ edited.attributes.title.should.equal('new title');
+
+ done();
+ }).then(null, done);
+ });
+
+ it('can add, defaulting as a draft', function (done) {
+ var createdPostUpdatedDate,
+ newPost = testUtils.DataGenerator.forModel.posts[2],
+ newPostDB = testUtils.DataGenerator.Content.posts[2];
+
+ PostModel.add(newPost).then(function (createdPost) {
+ return new PostModel({id: createdPost.id}).fetch();
+ }).then(function (createdPost) {
+ should.exist(createdPost);
+ createdPost.has('uuid').should.equal(true);
+ createdPost.get('status').should.equal('draft');
+ createdPost.get('title').should.equal(newPost.title, 'title is correct');
+ createdPost.get('markdown').should.equal(newPost.markdown, 'markdown is correct');
+ createdPost.has('html').should.equal(true);
+ createdPost.get('html').should.equal(newPostDB.html);
+ createdPost.get('slug').should.equal(newPostDB.slug + '-2');
+ createdPost.get('created_at').should.be.above(new Date(0).getTime());
+ createdPost.get('created_by').should.equal(1);
+ createdPost.get('author_id').should.equal(1);
+ createdPost.get('created_by').should.equal(createdPost.get('author_id'));
+ createdPost.get('updated_at').should.be.above(new Date(0).getTime());
+ createdPost.get('updated_by').should.equal(1);
+ should.equal(createdPost.get('published_at'), null);
+ should.equal(createdPost.get('published_by'), null);
+
+ createdPostUpdatedDate = createdPost.get('updated_at');
+
+ // Set the status to published to check that `published_at` is set.
+ return createdPost.save({status: 'published'});
+ }).then(function (publishedPost) {
+ publishedPost.get('published_at').should.be.instanceOf(Date);
+ publishedPost.get('published_by').should.equal(1);
+ publishedPost.get('updated_at').should.be.instanceOf(Date);
+ publishedPost.get('updated_by').should.equal(1);
+ publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate);
+
+ done();
+ }).then(null, done);
+
+ });
+
+ it('can trim title', function (done) {
+ var untrimmedCreateTitle = ' test trimmed create title ',
+ untrimmedUpdateTitle = ' test trimmed update title ',
+ newPost = {
+ title: untrimmedCreateTitle,
+ markdown: 'Test Content'
+ };
+
+ PostModel.add(newPost).then(function (createdPost) {
+ return new PostModel({ id: createdPost.id }).fetch();
+ }).then(function (createdPost) {
+ should.exist(createdPost);
+ createdPost.get('title').should.equal(untrimmedCreateTitle.trim());
+
+ return createdPost.save({ title: untrimmedUpdateTitle });
+ }).then(function (updatedPost) {
+ updatedPost.get('title').should.equal(untrimmedUpdateTitle.trim());
+
+ done();
+ }).otherwise(done);
+ });
+
+ it('can generate a non conflicting slug', function (done) {
+ var newPost = {
+ title: 'Test Title',
+ markdown: 'Test Content 1'
+ };
+
+ this.timeout(5000); // this is a patch to ensure it doesn't timeout.
+
+ // Create 12 posts with the same title
+ sequence(_.times(12, function (i) {
+ return function () {
+ return PostModel.add({
+ title: 'Test Title',
+ markdown: 'Test Content ' + (i+1)
+ });
+ };
+ })).then(function (createdPosts) {
+ // Should have created 12 posts
+ createdPosts.length.should.equal(12);
+
+ // Should have unique slugs and contents
+ _(createdPosts).each(function (post, i) {
+ var num = i + 1;
+
+ // First one has normal title
+ if (num === 1) {
+ post.get('slug').should.equal('test-title');
+ return;
+ }
+
+ post.get('slug').should.equal('test-title-' + num);
+ post.get('markdown').should.equal('Test Content ' + num);
+ });
+
+ done();
+ }).otherwise(done);
+ });
+
+ it('can generate slugs without duplicate hyphens', function (done) {
+ var newPost = {
+ title: 'apprehensive titles have too many spaces ',
+ markdown: 'Test Content 1'
+ };
+
+ PostModel.add(newPost).then(function (createdPost) {
+
+ createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces');
+
+ done();
+ }).then(null, done);
+ });
+
+ it('detects duplicate slugs before saving', function (done) {
+ var firstPost = {
+ title: 'First post',
+ markdown: 'First content 1'
+ },
+ secondPost = {
+ title: 'Second post',
+ markdown: 'Second content 1'
+ };
+
+ // Create the first post
+ PostModel.add(firstPost)
+ .then(function (createdFirstPost) {
+ // Store the slug for later
+ firstPost.slug = createdFirstPost.get('slug');
+
+ // Create the second post
+ return PostModel.add(secondPost);
+ }).then(function (createdSecondPost) {
+ // Store the slug for comparison later
+ secondPost.slug = createdSecondPost.get('slug');
+
+ // Update with a conflicting slug from the first post
+ return createdSecondPost.save({
+ slug: firstPost.slug
+ });
+ }).then(function (updatedSecondPost) {
+
+ // Should have updated from original
+ updatedSecondPost.get('slug').should.not.equal(secondPost.slug);
+ // Should not have a conflicted slug from the first
+ updatedSecondPost.get('slug').should.not.equal(firstPost.slug);
+
+ return PostModel.read({
+ id: updatedSecondPost.id
+ });
+ }).then(function (foundPost) {
+
+ // Should have updated from original
+ foundPost.get('slug').should.not.equal(secondPost.slug);
+ // Should not have a conflicted slug from the first
+ foundPost.get('slug').should.not.equal(firstPost.slug);
+
+ done();
+ }).otherwise(done);
+ });
+
+ it('can delete', function (done) {
+ var firstPostId;
+ PostModel.browse().then(function (results) {
+ should.exist(results);
+ results.length.should.be.above(0);
+ firstPostId = results.models[0].id;
+
+ return PostModel.destroy(firstPostId);
+ }).then(function () {
+ return PostModel.browse();
+ }).then(function (newResults) {
+ var ids, hasDeletedId;
+
+ ids = _.pluck(newResults.models, 'id');
+ hasDeletedId = _.any(ids, function (id) {
+ return id === firstPostId;
+ });
+ hasDeletedId.should.equal(false);
+
+ done();
+ }).then(null, done);
+ });
+
+ it('can create a new Post with a previous published_at date', function (done) {
+ var previousPublishedAtDate = new Date(2013, 8, 21, 12);
+
+ PostModel.add({
+ status: 'published',
+ published_at: previousPublishedAtDate,
+ title: 'published_at test',
+ markdown: 'This is some content'
+ }).then(function (newPost) {
+
+ should.exist(newPost);
+
+ newPost.get('published_at').should.equal(previousPublishedAtDate);
+
+ done();
+
+ }).otherwise(done);
+ });
+
+ it('can fetch a paginated set, with various options', function (done) {
+ this.timeout(10000); // this is a patch to ensure it doesn't timeout.
+
+ testUtils.insertMorePosts().then(function () {
+
+ return PostModel.findPage({page: 2});
+ }).then(function (paginationResult) {
+ paginationResult.page.should.equal(2);
+ paginationResult.limit.should.equal(15);
+ paginationResult.posts.length.should.equal(15);
+ paginationResult.pages.should.equal(4);
+
+ return PostModel.findPage({page: 5});
+ }).then(function (paginationResult) {
+ paginationResult.page.should.equal(5);
+ paginationResult.limit.should.equal(15);
+ paginationResult.posts.length.should.equal(0);
+ paginationResult.pages.should.equal(4);
+
+ return PostModel.findPage({limit: 30});
+ }).then(function (paginationResult) {
+ paginationResult.page.should.equal(1);
+ paginationResult.limit.should.equal(30);
+ paginationResult.posts.length.should.equal(30);
+ paginationResult.pages.should.equal(2);
+
+ return PostModel.findPage({limit: 10, page: 2, where: {language: 'fr'}});
+ }).then(function (paginationResult) {
+ paginationResult.page.should.equal(2);
+ paginationResult.limit.should.equal(10);
+ paginationResult.posts.length.should.equal(10);
+ paginationResult.pages.should.equal(3);
+
+ return PostModel.findPage({limit: 10, page: 2, status: 'all'});
+ }).then(function (paginationResult) {
+ paginationResult.pages.should.equal(11);
+
+ done();
+ }).then(null, done);
+ });
+});
diff --git a/core/test/unit/model_roles_spec.js b/core/test/unit/model_roles_spec.js
new file mode 100644
index 0000000000..cbf6852216
--- /dev/null
+++ b/core/test/unit/model_roles_spec.js
@@ -0,0 +1,101 @@
+/*globals describe, it, before, beforeEach, afterEach */
+var testUtils = require('./testUtils'),
+ should = require('should'),
+ errors = require('../../server/errorHandling'),
+
+ // Stuff we are testing
+ Models = require('../../server/models');
+
+describe("Role Model", function () {
+
+ var RoleModel = Models.Role;
+
+ should.exist(RoleModel);
+
+ before(function (done) {
+ testUtils.clearData().then(function () {
+ done();
+ }, done);
+ });
+
+ beforeEach(function (done) {
+ this.timeout(5000);
+ testUtils.initData().then(function () {
+ done();
+ }, done);
+ });
+
+ afterEach(function (done) {
+ testUtils.clearData().then(function () {
+ done();
+ }, done);
+ });
+
+ it("can browse roles", function (done) {
+ RoleModel.browse().then(function (foundRoles) {
+ should.exist(foundRoles);
+
+ foundRoles.models.length.should.be.above(0);
+
+ done();
+ }).then(null, done);
+ });
+
+ it("can read roles", function (done) {
+ RoleModel.read({id: 1}).then(function (foundRole) {
+ should.exist(foundRole);
+
+ done();
+ }).then(null, done);
+ });
+
+ it("can edit roles", function (done) {
+ RoleModel.read({id: 1}).then(function (foundRole) {
+ should.exist(foundRole);
+
+ return foundRole.set({name: "updated"}).save();
+ }).then(function () {
+ return RoleModel.read({id: 1});
+ }).then(function (updatedRole) {
+ should.exist(updatedRole);
+
+ updatedRole.get("name").should.equal("updated");
+
+ done();
+ }).then(null, done);
+ });
+
+ it("can add roles", function (done) {
+ var newRole = {
+ name: "test1",
+ description: "test1 description"
+ };
+
+ RoleModel.add(newRole).then(function (createdRole) {
+ should.exist(createdRole);
+
+ createdRole.attributes.name.should.equal(newRole.name);
+ createdRole.attributes.description.should.equal(newRole.description);
+
+ done();
+ }).then(null, done);
+ });
+
+ it("can delete roles", function (done) {
+ RoleModel.read({id: 1}).then(function (foundRole) {
+ should.exist(foundRole);
+
+ return RoleModel['delete'](1);
+ }).then(function () {
+ return RoleModel.browse();
+ }).then(function (foundRoles) {
+ var hasRemovedId = foundRoles.any(function (role) {
+ return role.id === 1;
+ });
+
+ hasRemovedId.should.equal(false);
+
+ done();
+ }).then(null, done);
+ });
+});
\ No newline at end of file
diff --git a/core/test/unit/api_settings_spec.js b/core/test/unit/model_settings_spec.js
similarity index 100%
rename from core/test/unit/api_settings_spec.js
rename to core/test/unit/model_settings_spec.js
diff --git a/core/test/unit/api_tags_spec.js b/core/test/unit/model_tags_spec.js
similarity index 95%
rename from core/test/unit/api_tags_spec.js
rename to core/test/unit/model_tags_spec.js
index 072db66b9d..7dfaa93489 100644
--- a/core/test/unit/api_tags_spec.js
+++ b/core/test/unit/model_tags_spec.js
@@ -36,8 +36,8 @@ describe('Tag Model', function () {
var PostModel = Models.Post;
it('can add a tag', function (done) {
- var newPost = {title: 'Test Title 1', markdown: 'Test Content 1'},
- newTag = {name: 'tag1'},
+ var newPost = testUtils.DataGenerator.forModel.posts[0],
+ newTag = testUtils.DataGenerator.forModel.tags[0],
createdPostID;
when.all([
@@ -62,8 +62,8 @@ describe('Tag Model', function () {
// The majority of this test is ripped from above, which is obviously a Bad Thing.
// Would be nice to find a way to seed data with relations for cases like this,
// because there are more DB hits than needed
- var newPost = {title: 'Test Title 1', markdown: 'Test Content 1'},
- newTag = {name: 'tag1'},
+ var newPost = testUtils.DataGenerator.forModel.posts[0],
+ newTag = testUtils.DataGenerator.forModel.tags[0],
createdTagID,
createdPostID;
@@ -96,13 +96,12 @@ describe('Tag Model', function () {
function seedTags(tagNames) {
var createOperations = [
- PostModel.add({title: 'title', markdown: 'content'})
+ PostModel.add(testUtils.DataGenerator.forModel.posts[0])
];
var tagModels = tagNames.map(function (tagName) { return TagModel.add({name: tagName}); });
createOperations = createOperations.concat(tagModels);
-
return when.all(createOperations).then(function (models) {
var postModel = models[0],
attachOperations;
@@ -209,7 +208,7 @@ describe('Tag Model', function () {
it('can add a tag to a post on creation', function (done) {
- var newPost = {title: 'Test Title 1', markdown: 'Test Content 1', tags: [{name: 'test_tag_1'}]};
+ var newPost = _.extend(testUtils.DataGenerator.forModel.posts[0], {tags: [{name: 'test_tag_1'}]})
PostModel.add(newPost).then(function (createdPost) {
return PostModel.read({id: createdPost.id}, { withRelated: ['tags']});
diff --git a/core/test/unit/api_users_spec.js b/core/test/unit/model_users_spec.js
similarity index 93%
rename from core/test/unit/api_users_spec.js
rename to core/test/unit/model_users_spec.js
index 5693983216..54d888078c 100644
--- a/core/test/unit/api_users_spec.js
+++ b/core/test/unit/model_users_spec.js
@@ -33,11 +33,7 @@ describe('User Model', function run() {
});
it('can add first', function (done) {
- var userData = {
- name: 'test',
- password: 'testpass1',
- email: "test@test1.com"
- };
+ var userData = testUtils.DataGenerator.forModel.users[0];
UserModel.add(userData).then(function (createdUser) {
should.exist(createdUser);
@@ -64,11 +60,7 @@ describe('User Model', function run() {
});
it('can\'t add second', function (done) {
- var userData = {
- name: 'test',
- password: 'testpass3',
- email: "test3@test1.com"
- };
+ var userData = testUtils.DataGenerator.forModel.users[1];
return UserModel.add(userData).then(done, function (failure) {
failure.message.should.eql('A user is already registered. Only one user for now!');
diff --git a/core/test/unit/permissions_spec.js b/core/test/unit/permissions_spec.js
index 468e8aab7f..e93f0cca06 100644
--- a/core/test/unit/permissions_spec.js
+++ b/core/test/unit/permissions_spec.js
@@ -13,7 +13,7 @@ var testUtils = require('./testUtils'),
PermissionsProvider = Models.Permission,
PostProvider = Models.Post;
-describe('permissions', function () {
+describe('Permissions', function () {
before(function (done) {
testUtils.clearData()
diff --git a/core/test/unit/testUtils.js b/core/test/unit/testUtils.js
index 7f1310c881..766ae347c9 100644
--- a/core/test/unit/testUtils.js
+++ b/core/test/unit/testUtils.js
@@ -2,92 +2,81 @@ var knex = require('../../server/models/base').Knex,
when = require('when'),
migration = require("../../server/data/migration/"),
Settings = require('../../server/models/settings').Settings,
- testUtils,
- samplePost,
- sampleUser,
- sampleUserRole;
+ DataGenerator = require('./fixtures/data-generator'),
+ API = require('./utils/api');
-samplePost = function (i, status, lang) {
- lang = lang || 'en_us';
- status = status || 'draft';
- return {
- uuid: 'uuid1234',
- title: "Test Post " + i,
- slug: "ghost-from-fiction-to-function-" + i,
- markdown: "Three days ago I released a concept page<\/a> for a lite version of WordPress that I've been thinking about for a long time, called Ghost. I think it's fair to say that I didn't quite anticipate how strong the reaction would be - and I've hardly had time to catch my breath in the last 72 hours.\n\nThe response was overwhelming, and overwhelmingly positive. In the first 6 hours my site got 35,000 page views after hitting the number 1 slot on Hacker News<\/a>. As of right now, the traffic count is just over 91,000 page views<\/a> - and Ghost has been featured all over the place. Notable mentions so far include Christina Warren from Mashable, who wrote about it<\/a>. Michael Carney from PandoDaily interviewed me about it<\/a>. Someone even wrote about it in Chinese<\/a>. That's pretty cool.\n\n\nThe feedback has been amazing, and while it's impossible to reply to all of the messages individually, I'm getting to as many of them as I can and I want to thank each and every one of you who took the time to send me a message or share the concept because you liked it. Now that the initial storm has died down a bit, I wanted to take some time to answer some of the more common questions and talk about what's next.\n Three days ago I released a concept page<\/a> for a lite version of WordPress that I've been thinking about for a long time, called Ghost. I think it's fair to say that I didn't quite anticipate how strong the reaction would be - and I've hardly had time to catch my breath in the last 72 hours.<\/p>\n The response was overwhelming, and overwhelmingly positive. In the first 6 hours my site got 35,000 page views after hitting the number 1 slot on Hacker News<\/a>. As of right now, the traffic count is just over 91,000 page views<\/a> - and Ghost has been featured all over the place. Notable mentions so far include Christina Warren from Mashable, who wrote about it<\/a>. Michael Carney from PandoDaily interviewed me about it<\/a>. Someone even wrote about it in Chinese<\/a>. That's pretty cool.\\n The feedback has been amazing, and while it's impossible to reply to all of the messages individually, I'm getting to as many of them as I can and I want to thank each and every one of you who took the time to send me a message or share the concept because you liked it. Now that the initial storm has died down a bit, I wanted to take some time to answer some of the more common questions and talk about what's next.<\/p>\n The most common question, bizarrely:<\/p>\n This was really the only negativity I got in response to the post, and it surprised me. I put together the concept page as... just that... a concept. It was a way for me to get the ideas out of my head and \"down on paper\" - or so to speak. I used photoshop as a tool<\/em> to write down my idea with text and images. If I used a sketchbook as a tool <\/em>to create images and handwritten notes, then uploaded scans of it, I doubt anyone would complain. The concept page was never supposed to be a finished product because I had no idea if there would be any interest in it. I had no motivation to waste hours coding a custom layout for something might only ever be read by a few people and then forgotten.<\/p>\n Hardware manufacturers make hundreds of foam cutout prototypes of products before they build one with working buttons and screens. I'm aware of all the usability problems with a web page made of images, and equally, foam cutouts without buttons or screens aren't particularly user friendly either. They're not supposed to be.<\/p>\n Let's move on.<\/p>\n Because comments add a layer of complexity that is beyond the core focus of this platform, which is publishing. Again, that's not to say you couldn't have any comments. This could easily be added with a dedicated plugin where you own the data or (as mentioned) there are third party providers such as Disqus, IntenseDebate, Livefyre and Facebook who all have great platforms. The point of this isn't to say \"you can't have comments\" - it's to say \"comments aren't on by default\". It's about simplicity, more than anything else.<\/p>\n Sorry, but Tumblr already did this - it's not the future of blogging, it's the past.<\/p>\n Ghost isn't about sharing \"Fuck Yeah [Dogs<\/a>\/Sharks<\/a>\/Girls with Tattoos<\/a>]\" - it's about publishing - which means writing - rather than mashing a few buttons to make sure that everyone can see and appreciate your latest funny picture\/status, which is surely the most funny picture\/status you've ever posted.<\/p>\n Tumblr, Pinterest and Facebook already have this locked down. It's not the future.<\/p>\n The concept page was a way for me to test demand and interest. To see if anyone actually agreed with my frustrations and, more importantly, my solutions. I plucked a random figure of \"10,000 pageviews\" out of the air before I hit the publish button. If it got less than 10,000 pageviews, I would surrender to the fact that it would only ever be an idea. I've now exceeded that goal 9 times over, so yes, I'm looking at how Ghost can now be made into a reality.<\/p>\nFAQ - Continued...<\/h2>\n
Oh my god, why is that whole page made of images? What's wrong with you? \/\/ I can't take you seriously \/\/ Don't you know anything about the web? \/\/ You are literally Satan re-incarnate.<\/strong><\/em><\/h5>\n
What? Why no comments? I need comments.<\/strong><\/em><\/h5>\n
Yeah, but WordPress are already going to revise their dashboard, WordPress.com is experimenting with a potential simplified version... so why bother with this?<\/strong><\/em><\/h5>\n
<\/a><\/p>\n
So... are you actually going to build this thing?<\/strong><\/em><\/h5>\n
How can I find out when it's done? \/\/ SHUT UP AND TAKE MY MONEY<\/strong><\/em><\/h5>\n
\nHow are you going to do this?<\/h3>\n
\n
\n
\n
What's the answer?<\/h3>\n
FAQ - Continued...<\/h2>\n\nThe most common question, bizarrely:\n
Oh my god, why is that whole page made of images? What's wrong with you? \/\/ I can't take you seriously \/\/ Don't you know anything about the web? \/\/ You are literally Satan re-incarnate.<\/strong><\/em><\/h5>\n\nThis was really the only negativity I got in response to the post, and it surprised me. I put together the concept page as... just that... a concept. It was a way for me to get the ideas out of my head and \"down on paper\" - or so to speak. I used photoshop as a tool<\/em> to write down my idea with text and images. If I used a sketchbook as a tool <\/em>to create images and handwritten notes, then uploaded scans of it, I doubt anyone would complain. The concept page was never supposed to be a finished product because I had no idea if there would be any interest in it. I had no motivation to waste hours coding a custom layout for something might only ever be read by a few people and then forgotten.\n\nHardware manufacturers make hundreds of foam cutout prototypes of products before they build one with working buttons and screens. I'm aware of all the usability problems with a web page made of images, and equally, foam cutouts without buttons or screens aren't particularly user friendly either. They're not supposed to be.\n\nLet's move on.\n
What? Why no comments? I need comments.<\/strong><\/em><\/h5>\n\nBecause comments add a layer of complexity that is beyond the core focus of this platform, which is publishing. Again, that's not to say you couldn't have any comments. This could easily be added with a dedicated plugin where you own the data or (as mentioned) there are third party providers such as Disqus, IntenseDebate, Livefyre and Facebook who all have great platforms. The point of this isn't to say \"you can't have comments\" - it's to say \"comments aren't on by default\". It's about simplicity, more than anything else.\n
Yeah, but WordPress are already going to revise their dashboard, WordPress.com is experimenting with a potential simplified version... so why bother with this?<\/strong><\/em><\/h5>\n\n
<\/a>\n\nSorry, but Tumblr already did this - it's not the future of blogging, it's the past.\n\nGhost isn't about sharing \"Fuck Yeah [Dogs<\/a>\/Sharks<\/a>\/Girls with Tattoos<\/a>]\" - it's about publishing - which means writing - rather than mashing a few buttons to make sure that everyone can see and appreciate your latest funny picture\/status, which is surely the most funny picture\/status you've ever posted.\n\nTumblr, Pinterest and Facebook already have this locked down. It's not the future.\n
So... are you actually going to build this thing?<\/strong><\/em><\/h5>\n\nThe concept page was a way for me to test demand and interest. To see if anyone actually agreed with my frustrations and, more importantly, my solutions. I plucked a random figure of \"10,000 pageviews\" out of the air before I hit the publish button. If it got less than 10,000 pageviews, I would surrender to the fact that it would only ever be an idea. I've now exceeded that goal 9 times over, so yes, I'm looking at how Ghost can now be made into a reality.\n
How can I find out when it's done? \/\/ SHUT UP AND TAKE MY MONEY<\/strong><\/em><\/h5>\n\nOk, ok - there's a holding page up on http:\/\/TryGhost.org<\/a> - put your email address in.\n
\nHow are you going to do this?<\/h3>\n\nThere's three main ways of going about this, each has merits as well as drawbacks.\n\n1.) Build it from scratch<\/strong><\/em> - Many people (particularly the Hacker News crowd) expressed the sentiment that there was little point in forking WordPress. When you're going to strip out so much, you get to a point where you might as well start from scratch anyway. Take away the crutches of being stuck with older technologies and put together something which is as sophisticated in code as it is in UI\/UX.\n
\n
\n
\n
What's the answer?<\/h3>\n\nI've spoken to a lot of smart people over the last few days. The one thing that everyone seems to agree on is that a fork is the worst of both worlds. So the one thing that I suggested as a way of making this happen, is the least likely to work in reality. Remember the foam prototype metaphor earlier? Learning and iterating - that's what happening now.\n\nThat leaves a choice between WordPress plugin or fresh build. The answer? Both.\n\nA WordPress plugin will act as a proof of concept and a working prototype, initially, because it's easier to leverage the existing WordPress ecosystem to create it than to go into a cave for 6 months trying to build this amazing thing that everyone will have forgotten about.\n\nThe plugin will not be perfect. It will add the Ghost UI\/UX and as much functionality as we can cram into it. It will completely remove \/wp-admin\/ and replace it with \/ghost\/ - effectively using WordPress core as a basic foundation to build on top of. It will give people who don't want to switch away from WordPress access to the Ghost UX which they want to have, and it will give people who want the full Ghost platform a taste of what's to come.\n\nIt will allow us to develop and learn and iterate on the concept pretty rapidly, which has a great deal of value.\n\nThis is step one. Assuming the plugin is actually used by people - it would then justify exploring building the standalone version of Ghost from the ground up. The plugin would subsequently serve as a great marketing tool for the platform. Think of it as an upgrade path. But that's a long way away. Having the idea is the easy part. Making it happen is what counts.\n\nHappily - amongst the thousands of people talking about Ghost for the last few days - several have been talking about how they've already built some working prototypes of my mockups and turned them into WordPress plugins or just local development sites. These will likely go on to be the starting point of the first Ghost plugin.<\/p>\n\nThere's a lot to do, and I'm amazed by the number of people who have offered their help with this. In the next few days I'll be kicking off work on the plugin properly and start putting together a more organised structure which explains how you can get involved and contribute to the project if you're interested. So... watch this space - and thanks for all your support so far.\n\nFollow @TryGhost<\/a>",
- html: "
FAQ - Continued...<\/h2>\n
Oh my god, why is that whole page made of images? What's wrong with you? \/\/ I can't take you seriously \/\/ Don't you know anything about the web? \/\/ You are literally Satan re-incarnate.<\/strong><\/em><\/h5>\n
What? Why no comments? I need comments.<\/strong><\/em><\/h5>\n
Yeah, but WordPress are already going to revise their dashboard, WordPress.com is experimenting with a potential simplified version... so why bother with this?<\/strong><\/em><\/h5>\n
<\/a><\/p>\n
So... are you actually going to build this thing?<\/strong><\/em><\/h5>\n
How can I find out when it's done? \/\/ SHUT UP AND TAKE MY MONEY<\/strong><\/em><\/h5>\n