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('= 0, 'Feed should contain ') >= 0, 'Feed should contain '); }); - - casper.run(function () { - test.done(); - }); -}); \ No newline at end of file +}, true); \ No newline at end of file diff --git a/core/test/unit/api_posts_spec.js b/core/test/unit/api_posts_spec.js index 6731d9222b..47eacf876a 100644 --- a/core/test/unit/api_posts_spec.js +++ b/core/test/unit/api_posts_spec.js @@ -1,36 +1,32 @@ /*globals describe, before, beforeEach, afterEach, it */ var testUtils = require('./testUtils'), should = require('should'), - _ = require('underscore'), - when = require('when'), - sequence = require('when/sequence'), + _ = require('underscore'); - // Stuff we are testing - Models = require('../../server/models'); +describe('Post API', function () { -describe('Post Model', function () { - - var PostModel = Models.Post, - UserModel = Models.User, - userData = { - password: 'testpass1', - email: 'test@test1.com', - name: 'Mr Biscuits' - }; + var user = testUtils.DataGenerator.forModel.users[0], + authCookie; before(function (done) { - testUtils.clearData().then(function () { - done(); - }, done); + testUtils.clearData() + .then(function () { + done(); + }, done); }); beforeEach(function (done) { this.timeout(5000); testUtils.initData() .then(function () { - return UserModel.add(userData); + return testUtils.insertDefaultFixtures(); }) .then(function () { + return testUtils.API.login(user.email, user.password); + }) + .then(function (authResponse) { + authCookie = authResponse; + done(); }, done); }); @@ -41,330 +37,13 @@ describe('Post Model', function () { }, done); }); - it('can browse', function (done) { - PostModel.browse().then(function (results) { - should.exist(results); - results.length.should.equal(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('Mr Biscuits'); - firstPost.user.name.should.equal('Mr Biscuits'); - - done(); - }, done); - }); - - it('can findOne, returning author and user data', function (done) { - var firstPost; - - PostModel.findOne({}).then(function (result) { + it('can retrieve a post', function (done) { + testUtils.API.get(testUtils.API.ApiRouteBase + 'posts/?status=all', authCookie).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('Mr Biscuits'); - firstPost.user.name.should.equal('Mr Biscuits'); - - 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 = { - title: 'Test Title 1', - markdown: 'Test Content 1' - }; - - 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('

' + newPost.markdown + '

'); - createdPost.get('slug').should.equal('test-title-1'); - 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()); - + should.exist(result.response); + result.response.posts.length.should.be.above(1); 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/fixtures/data-generator.js b/core/test/unit/fixtures/data-generator.js new file mode 100644 index 0000000000..4b878f602a --- /dev/null +++ b/core/test/unit/fixtures/data-generator.js @@ -0,0 +1,217 @@ +var _ = require('underscore'), + uuid = require('node-uuid') + DataGenerator = {}; + +DataGenerator.Content = { + posts: [ + { + title: "HTML Ipsum", + slug: "html-ipsum", + markdown: "

HTML Ipsum Presents

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, 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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
" + }, + { + title: "Kitchen Sink", + slug: "kitchen-sink", + markdown: "

HTML Ipsum Presents

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, 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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
", + }, + { + title: "Short and Sweet", + slug: "short-and-sweet", + markdown: "## testing\n\nmctesters\n\n- test\n- line\n- items", + html: "

testing

\n\n

mctesters

\n\n" + }, + { + title: "Not so short, bit complex", + slug: "not-so-short-bit-complex", + 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.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

" + } + ], + + tags: [ + { + name: "kitchen sink", + slug: "kitchen-sink" + }, + { + name: "bacon", + slug: "bacon" + }, + { + name: "chorizo", + slug: "chorizo" + }, + { + name: "pollo", + slug: "pollo" + } + ], + + // Password = Sl1m3rson + users: [ + { + name: 'Joe Bloggs', + slug: 'joe-blogs', + email: 'jbloggs@example.com', + password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6' + }, + { + name: 'Smith Wellingsworth', + slug: 'smith-wellingsworth', + email: 'swellingsworth@example.com', + password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6' + }, + { + name: 'Jimothy Bogendath', + slug: 'jimothy-bogendath', + email: 'jbogendath@example.com', + password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6' + }, + { + name: 'Slimer McEctoplasm', + slug: 'slimer-mcectoplasm', + email: 'smcectoplasm@example.com', + password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6' + } + ] +}; + +DataGenerator.forKnex = (function () { + + var posts, + tags, + posts_tags; + + function createPost(overrides) { + return _.defaults(overrides, { + uuid: uuid.v4(), + status: 'published', + html: overrides.markdown, + language: 'en_US', + featured: true, + page: false, + author_id: 1, + updated_at: new Date(), + updated_by: 1, + created_at: new Date(), + created_by: 1, + published_at: new Date(), + published_by: 1 + }); + } + + function createGenericPost(uniqueInteger, status, language) { + status = status || 'draft'; + language = language || 'en_US'; + + return createPost({ + uuid: uuid.v4(), + title: 'Test Post ' + uniqueInteger, + slug: 'ghost-from-fiction-to-function-' + uniqueInteger, + 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

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
\n

How 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