From 15abb7a2579228f46734163193dbf4de11302ade Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 13 Nov 2015 11:48:59 +0000 Subject: [PATCH] Add acceptance test for setup flow happy-path refs #6039 - add `jquery-deparam` ember testing dependency for use in mirage config - setup necessary mirage fixtures & endpoints for successful testing of setup flow's happy-path - add happy-path acceptance test for setup flow --- core/client/app/mirage/config.js | 110 ++++++++++-- core/client/app/mirage/fixtures/roles.js | 42 +++++ core/client/app/routes/setup/one.js | 6 +- core/client/bower.json | 1 + core/client/ember-cli-build.js | 1 + .../tests/acceptance/settings/setup-test.js | 168 ++++++++++++++++++ 6 files changed, 314 insertions(+), 14 deletions(-) create mode 100644 core/client/app/mirage/fixtures/roles.js create mode 100644 core/client/tests/acceptance/settings/setup-test.js diff --git a/core/client/app/mirage/config.js b/core/client/app/mirage/config.js index 8b3b89b4cb..71df1ca2f9 100644 --- a/core/client/app/mirage/config.js +++ b/core/client/app/mirage/config.js @@ -1,12 +1,14 @@ import Ember from 'ember'; -const {isBlank} = Ember; +let {isBlank} = Ember; function paginatedResponse(modelName, allModels, request) { - const page = +request.queryParams.page || 1; + let page = +request.queryParams.page || 1; let limit = request.queryParams.limit || 15; let pages, models, next, prev; + allModels = allModels || []; + if (limit === 'all') { models = allModels; pages = 1; @@ -48,6 +50,27 @@ export default function () { this.namespace = 'ghost/api/v0.1'; // make this `api`, for example, if your API is namespaced // this.timing = 400; // delay for each request, automatically set to 0 during testing + /* Authentication ------------------------------------------------------- */ + + this.post('/authentication/token', function () { + return { + access_token: '5JhTdKI7PpoZv4ROsFoERc6wCHALKFH5jxozwOOAErmUzWrFNARuH1q01TYTKeZkPW7FmV5MJ2fU00pg9sm4jtH3Z1LjCf8D6nNqLYCfFb2YEKyuvG7zHj4jZqSYVodN2YTCkcHv6k8oJ54QXzNTLIDMlCevkOebm5OjxGiJpafMxncm043q9u1QhdU9eee3zouGRMVVp8zkKVoo5zlGMi3zvS2XDpx7xsfk8hKHpUgd7EDDQxmMueifWv7hv6n', + expires_in: 3600, + refresh_token: 'XP13eDjwV5mxOcrq1jkIY9idhdvN3R1Br5vxYpYIub2P5Hdc8pdWMOGmwFyoUshiEB62JWHTl8H1kACJR18Z8aMXbnk5orG28br2kmVgtVZKqOSoiiWrQoeKTqrRV0t7ua8uY5HdDUaKpnYKyOdpagsSPn3WEj8op4vHctGL3svOWOjZhq6F2XeVPMR7YsbiwBE8fjT3VhTB3KRlBtWZd1rE0Qo2EtSplWyjGKv1liAEiL0ndQoLeeSOCH4rTP7', + token_type: 'Bearer' + }; + }); + + /* Download Count ------------------------------------------------------- */ + + let downloadCount = 0; + this.get('http://ghost.org/count/', function () { + downloadCount++; + return { + count: downloadCount + }; + }); + /* Notifications -------------------------------------------------------- */ this.get('/notifications/', 'notifications'); @@ -55,13 +78,14 @@ export default function () { /* Posts ---------------------------------------------------------------- */ this.post('/posts/', function (db, request) { - const [attrs] = JSON.parse(request.requestBody).posts; + let [attrs] = JSON.parse(request.requestBody).posts; let post; if (isBlank(attrs.slug) && !isBlank(attrs.title)) { attrs.slug = attrs.title.dasherize(); } + // NOTE: this does not use the post factory to fill in blank fields post = db.posts.insert(attrs); return { @@ -69,11 +93,21 @@ export default function () { }; }); + this.get('/posts/', function (db, request) { + // TODO: handle status/staticPages/author params + let response = paginatedResponse('posts', db.posts, request); + return response; + }); + + /* Roles ---------------------------------------------------------------- */ + + this.get('/roles/', 'roles'); + /* Settings ------------------------------------------------------------- */ this.get('/settings/', function (db, request) { - const filters = request.queryParams.type.split(','), - settings = []; + let filters = request.queryParams.type.split(','); + let settings = []; filters.forEach(filter => { settings.pushObjects(db.settings.where({type: filter})); @@ -90,7 +124,7 @@ export default function () { }); this.put('/settings/', function (db, request) { - const newSettings = JSON.parse(request.requestBody); + let newSettings = JSON.parse(request.requestBody); db.settings.remove(); db.settings.insert(newSettings); @@ -111,16 +145,52 @@ export default function () { }; }); + /* Setup ---------------------------------------------------------------- */ + + this.post('/authentication/setup', function (db, request) { + let [attrs] = $.deparam(request.requestBody).setup; + let [role] = db.roles.where({name: 'Owner'}); + let user; + + // create owner role unless already exists + if (!role) { + role = db.roles.insert({name: 'Owner'}); + } + attrs.roles = [role]; + + if (!isBlank(attrs.email)) { + attrs.slug = attrs.email.split('@')[0].dasherize(); + } + + // NOTE: this does not use the user factory to fill in blank fields + user = db.users.insert(attrs); + + delete user.roles; + + return { + users: [user] + }; + }); + + this.get('/authentication/setup/', function () { + return { + setup: [ + {status: true} + ] + }; + }); + /* Tags ----------------------------------------------------------------- */ this.post('/tags/', function (db, request) { - const [attrs] = JSON.parse(request.requestBody).tags; + let [attrs] = JSON.parse(request.requestBody).tags; let tag; if (isBlank(attrs.slug) && !isBlank(attrs.name)) { attrs.slug = attrs.name.dasherize(); } + // NOTE: this does not use the tag factory to fill in blank fields tag = db.tags.insert(attrs); return { @@ -129,13 +199,13 @@ export default function () { }); this.get('/tags/', function (db, request) { - const response = paginatedResponse('tags', db.tags, request); + let response = paginatedResponse('tags', db.tags, request); // TODO: remove post_count unless requested? return response; }); this.get('/tags/slug/:slug/', function (db, request) { - const [tag] = db.tags.where({slug: request.params.slug}); + let [tag] = db.tags.where({slug: request.params.slug}); // TODO: remove post_count unless requested? @@ -145,9 +215,9 @@ export default function () { }); this.put('/tags/:id/', function (db, request) { - const id = request.params.id, - [attrs] = JSON.parse(request.requestBody).tags, - record = db.tags.update(id, attrs); + let id = request.params.id; + let [attrs] = JSON.parse(request.requestBody).tags; + let record = db.tags.update(id, attrs); return { tag: record @@ -158,6 +228,22 @@ export default function () { /* Users ---------------------------------------------------------------- */ + this.post('/users/', function (db, request) { + let [attrs] = JSON.parse(request.requestBody).users; + let user; + + if (!isBlank(attrs.email)) { + attrs.slug = attrs.email.split('@')[0].dasherize(); + } + + // NOTE: this does not use the user factory to fill in blank fields + user = db.users.insert(attrs); + + return { + users: [user] + }; + }); + // /users/me = Always return the user with ID=1 this.get('/users/me', function (db) { return { diff --git a/core/client/app/mirage/fixtures/roles.js b/core/client/app/mirage/fixtures/roles.js new file mode 100644 index 0000000000..768f67be7f --- /dev/null +++ b/core/client/app/mirage/fixtures/roles.js @@ -0,0 +1,42 @@ +export default [ + { + id: 1, + uuid: 'b2576c4e-fa4e-41d4-8236-ced75f735222', + name: 'Administrator', + description: 'Administrators', + created_at: '2015-11-13T16:01:29.131Z', + created_by: 1, + updated_at: '2015-11-13T16:01:29.131Z', + updated_by: 1 + }, + { + id: 2, + uuid: '6ee03efb-322e-4f6e-9c91-bc228b5eec6b', + name: 'Editor', + description: 'Editors', + created_at: '2015-11-13T16:01:29.131Z', + created_by: 1, + updated_at: '2015-11-13T16:01:29.131Z', + updated_by: 1 + }, + { + id: 3, + uuid: 'de481b62-63f8-42c7-b5b9-6c5f5a877f53', + name: 'Author', + description: 'Authors', + created_at: '2015-11-13T16:01:29.131Z', + created_by: 1, + updated_at: '2015-11-13T16:01:29.131Z', + updated_by: 1 + }, + { + id: 4, + uuid: 'ac8cbaf6-e6be-4129-b0fb-ec9ddfa61056', + name: 'Owner', + description: 'Blog Owner', + created_at: '2015-11-13T16:01:29.132Z', + created_by: 1, + updated_at: '2015-11-13T16:01:29.132Z', + updated_by: 1 + } +]; diff --git a/core/client/app/routes/setup/one.js b/core/client/app/routes/setup/one.js index 1d50af9337..d9f3b76f88 100644 --- a/core/client/app/routes/setup/one.js +++ b/core/client/app/routes/setup/one.js @@ -12,12 +12,14 @@ var DownloadCountPoller = Ember.Object.extend({ }, poll: function () { - var interval = 2000, + var interval = Ember.testing ? 20 : 2000, runId; runId = Ember.run.later(this, function () { this.downloadCounter(); - this.poll(); + if (!Ember.testing) { + this.poll(); + } }, interval); this.set('runId', runId); diff --git a/core/client/bower.json b/core/client/bower.json index 753a1e7269..6e8e56d8e3 100644 --- a/core/client/bower.json +++ b/core/client/bower.json @@ -16,6 +16,7 @@ "fastclick": "1.0.6", "google-caja": "5669.0.0", "jquery": "2.1.4", + "jquery-deparam": "~0.5.0", "jquery-file-upload": "9.5.6", "jquery-hammerjs": "1.0.1", "jquery-ui": "1.11.4", diff --git a/core/client/ember-cli-build.js b/core/client/ember-cli-build.js index 84de3285cf..342e69060f 100644 --- a/core/client/ember-cli-build.js +++ b/core/client/ember-cli-build.js @@ -70,6 +70,7 @@ module.exports = function (defaults) { if (app.env === 'test') { app.import('bower_components/jquery.simulate.drag-sortable/jquery.simulate.drag-sortable.js'); + app.import('bower_components/jquery-deparam/jquery-deparam.js'); } // 'dem Styles diff --git a/core/client/tests/acceptance/settings/setup-test.js b/core/client/tests/acceptance/settings/setup-test.js new file mode 100644 index 0000000000..0dcf1ed2ef --- /dev/null +++ b/core/client/tests/acceptance/settings/setup-test.js @@ -0,0 +1,168 @@ +/* jshint expr:true */ +import { + describe, + it, + beforeEach, + afterEach +} from 'mocha'; +import { expect } from 'chai'; +import Ember from 'ember'; +import startApp from '../../helpers/start-app'; +import { invalidateSession, authenticateSession } from 'ghost/tests/helpers/ember-simple-auth'; + +const {run} = Ember; + +describe('Acceptance: Setup', function () { + let application; + + beforeEach(function () { + application = startApp(); + }); + + afterEach(function () { + run(application, 'destroy'); + }); + + it('redirects if already authenticated', function () { + const role = server.create('role', {name: 'Author'}), + user = server.create('user', {roles: [role], slug: 'test-user'}); + + authenticateSession(application); + + visit('/setup/one'); + andThen(() => { + expect(currentURL()).to.equal('/'); + }); + + visit('/setup/two'); + andThen(() => { + expect(currentURL()).to.equal('/'); + }); + + visit('/setup/three'); + andThen(() => { + expect(currentURL()).to.equal('/'); + }); + }); + + it('redirects to signin if already set up', function () { + // mimick an already setup blog + server.get('/authentication/setup/', function () { + return { + setup: [ + {status: true} + ] + }; + }); + + invalidateSession(application); + + visit('/setup'); + andThen(() => { + expect(currentURL()).to.equal('/signin'); + }); + }); + + it('has a successful happy path', function () { + // mimick a new blog + server.get('/authentication/setup/', function () { + return { + setup: [ + {status: false} + ] + }; + }); + + invalidateSession(application); + server.loadFixtures('roles'); + + visit('/setup'); + + andThen(() => { + // it redirects to step one + expect(currentURL(), 'url after accessing /setup') + .to.equal('/setup/one'); + + // it highlights first step + expect(find('.gh-flow-nav .step:first-of-type').hasClass('active')) + .to.be.true; + expect(find('.gh-flow-nav .step:nth-of-type(2)').hasClass('active')) + .to.be.false; + expect(find('.gh-flow-nav .step:nth-of-type(3)').hasClass('active')) + .to.be.false; + + // it displays download count (count increments for each ajax call + // and polling is disabled in testing so our count should be "2" - + // 1 for first load and 1 for first poll) + expect(find('.gh-flow-content em').text()).to.equal('2'); + }); + + click('.btn-green'); + + andThen(() => { + // it transitions to step two + expect(currentURL(), 'url after clicking "Create your account"') + .to.equal('/setup/two'); + + // email field is focused by default + // NOTE: $('x').is(':focus') doesn't work in phantomjs CLI runner + // https://github.com/ariya/phantomjs/issues/10427 + expect(find('[name="email"]').get(0) === document.activeElement, 'email field has focus') + .to.be.true; + }); + + click('.btn-green'); + + andThen(() => { + // it marks fields as invalid + expect(find('.form-group.error').length, 'number of invalid fields') + .to.equal(4); + + // it displays error messages + expect(find('.error .response').length, 'number of in-line validation messages') + .to.equal(4); + + // it displays main error + expect(find('.main-error').length, 'main error is displayed') + .to.equal(1); + }); + + // enter valid details and submit + fillIn('[name="email"]', 'test@example.com'); + fillIn('[name="name"]', 'Test User'); + fillIn('[name="password"]', 'password'); + fillIn('[name="blog-title"]', 'Blog Title'); + click('.btn-green'); + + andThen(() => { + // it transitions to step 3 + expect(currentURL(), 'url after submitting step two') + .to.equal('/setup/three'); + + // submit button is "disabled" + expect(find('button[type="submit"]').hasClass('btn-green'), 'invite button with no emails is white') + .to.be.false; + }); + + // fill in a valid email + fillIn('[name="users"]', 'new-user@example.com'); + + andThen(() => { + // submit button is "enabled" + expect(find('button[type="submit"]').hasClass('btn-green'), 'invite button is green with valid email address') + .to.be.true; + }); + + // submit the invite form + click('button[type="submit"]'); + + andThen(() => { + // it redirects to the home / "content" screen + expect(currentURL(), 'url after submitting invites') + .to.equal('/'); + }); + }); + + it('handles server validation errors in step 2'); + it('handles server validation errors in step 3'); +});