Commit f0c912bf1f2a4157f8583e1922edfd5247346701

Authored by tmcdeveloper
1 parent d1f05315dd
Exists in master

add method tests and airbnb linting via eslint

... ... @@ -36,3 +36,5 @@ practicalmeteor:mocha
36 36 xolvio:backdoor
37 37 xolvio:email-stub
38 38 mdg:validated-method
  39 +dburles:factory
  40 +aldeed:simple-schema
... ...
... ... @@ -25,6 +25,7 @@ caching-html-compiler@1.0.5
25 25 callback-hook@1.0.7
26 26 check@1.1.3
27 27 coffeescript@1.0.16
  28 +dburles:factory@0.4.2
28 29 ddp@1.2.4
29 30 ddp-client@1.2.4
30 31 ddp-common@1.2.4
... ... @@ -38,8 +39,8 @@ ejson@1.0.10
38 39 email@1.0.11
39 40 es5-shim@4.5.9
40 41 fastclick@1.0.10
41   -fortawesome:fontawesome@4.4.0_1
42   -fourseven:scss@3.4.1
  42 +fortawesome:fontawesome@4.5.0
  43 +fourseven:scss@3.4.3
43 44 geojson-utils@1.0.7
44 45 hot-code-push@1.0.3
45 46 html-tools@1.0.8
... ... @@ -51,7 +52,7 @@ launch-screen@1.0.10
51 52 livedata@1.0.17
52 53 localstorage@1.0.8
53 54 logging@1.0.11
54   -mdg:validated-method@1.0.2
  55 +mdg:validated-method@1.1.0
55 56 mdg:validation-error@0.5.1
56 57 meteor@1.1.13
57 58 meteor-base@1.0.3
... ... @@ -70,14 +71,14 @@ observe-sequence@1.0.10
70 71 ordered-dict@1.0.6
71 72 practicalmeteor:chai@2.1.0_1
72 73 practicalmeteor:loglevel@1.2.0_2
73   -practicalmeteor:mocha@2.1.0_8
  74 +practicalmeteor:mocha@2.4.5_1
74 75 practicalmeteor:mocha-core@0.1.4
75 76 practicalmeteor:sinon@1.14.1_2
76 77 promise@0.6.6
77 78 raix:eventemitter@0.1.3
78 79 random@1.0.8
79 80 rate-limit@1.0.3
80   -react-meteor-data@0.2.7
  81 +react-meteor-data@0.2.9
81 82 reactive-dict@1.1.6
82 83 reactive-var@1.0.8
83 84 reload@1.1.7
... ... @@ -104,6 +105,6 @@ underscore@1.0.7
104 105 url@1.0.8
105 106 webapp@1.2.7
106 107 webapp-hashing@1.0.8
107   -xolvio:backdoor@0.1.2
108   -xolvio:cleaner@0.2.0
  108 +xolvio:backdoor@0.2.0
  109 +xolvio:cleaner@0.3.0
109 110 xolvio:email-stub@0.2.0
... ...
imports/api/documents/documents.js
1 1 import { Mongo } from 'meteor/mongo';
  2 +import faker from 'faker';
2 3  
3   -export const Documents = new Mongo.Collection( 'Documents' );
  4 +export const Documents = new Mongo.Collection('Documents');
  5 +
  6 +Documents.schema = new SimpleSchema({
  7 + title: {
  8 + type: String,
  9 + label: 'The title of the document.',
  10 + },
  11 +});
  12 +
  13 +Documents.attachSchema(Documents.schema);
  14 +
  15 +Factory.define('document', Documents, {
  16 + title: () => faker.hacker.phrase(),
  17 +});
... ...
imports/api/documents/documents.tests.js
... ... @@ -0,0 +1,13 @@
  1 +/* eslint-env mocha */
  2 +/* eslint-disable func-names, prefer-arrow-callback */
  3 +
  4 +import { chai } from 'meteor/practicalmeteor:chai';
  5 +import { Documents } from './documents.js';
  6 +
  7 +const { assert } = chai;
  8 +
  9 +describe('Documents collection', function () {
  10 + it('registers the collection with Mongo properly', function () {
  11 + assert.equal(typeof Documents, 'object');
  12 + });
  13 +});
... ...
imports/api/documents/methods.js
... ... @@ -3,30 +3,30 @@ import { Documents } from './documents';
3 3 export const insertDocument = new ValidatedMethod({
4 4 name: 'documents.insert',
5 5 validate: new SimpleSchema({
6   - title: { type: String }
  6 + title: { type: String },
7 7 }).validator(),
8   - run( document ) {
9   - Documents.insert( document );
10   - }
  8 + run(document) {
  9 + Documents.insert(document);
  10 + },
11 11 });
12 12  
13 13 export const updateDocument = new ValidatedMethod({
14 14 name: 'documents.update',
15 15 validate: new SimpleSchema({
16 16 _id: { type: String },
17   - 'update.title': { type: String, optional: true }
  17 + 'update.title': { type: String, optional: true },
18 18 }).validator(),
19   - run( { _id, update } ) {
20   - Documents.update( _id, { $set: update } );
21   - }
  19 + run({ _id, update }) {
  20 + Documents.update(_id, { $set: update });
  21 + },
22 22 });
23 23  
24 24 export const removeDocument = new ValidatedMethod({
25 25 name: 'documents.remove',
26 26 validate: new SimpleSchema({
27   - _id: { type: String }
  27 + _id: { type: String },
28 28 }).validator(),
29   - run( { _id } ) {
30   - Documents.remove( _id );
31   - }
  29 + run({ _id }) {
  30 + Documents.remove(_id);
  31 + },
32 32 });
... ...
imports/api/documents/methods.tests.js
... ... @@ -0,0 +1,42 @@
  1 +/* eslint-env mocha */
  2 +/* eslint-disable func-names, prefer-arrow-callback */
  3 +
  4 +import { assert } from 'meteor/practicalmeteor:chai';
  5 +import { resetDatabase } from 'meteor/xolvio:cleaner';
  6 +import { Documents } from './documents.js';
  7 +import { insertDocument, updateDocument, removeDocument } from './methods.js';
  8 +
  9 +describe('Documents methods', function () {
  10 + beforeEach(function () {
  11 + if (Meteor.isServer) {
  12 + resetDatabase();
  13 + }
  14 + });
  15 +
  16 + it('inserts a document into the Documents collection', function () {
  17 + insertDocument.call({ title: 'You can\'t arrest me, I\'m the Cake Boss!' });
  18 + const getDocument = Documents.findOne({ title: 'You can\'t arrest me, I\'m the Cake Boss!' });
  19 + assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!');
  20 + });
  21 +
  22 + it('updates a document in the Documents collection', function () {
  23 + const { _id } = Factory.create('document');
  24 +
  25 + updateDocument.call({
  26 + _id,
  27 + update: {
  28 + title: 'You can\'t arrest me, I\'m the Cake Boss!',
  29 + },
  30 + });
  31 +
  32 + const getDocument = Documents.findOne(_id);
  33 + assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!');
  34 + });
  35 +
  36 + it('removes a document from the Documents collection', function () {
  37 + const { _id } = Factory.create('document');
  38 + removeDocument.call({ _id });
  39 + const getDocument = Documents.findOne(_id);
  40 + assert.equal(getDocument, undefined);
  41 + });
  42 +});
... ...
imports/api/documents/server/publications.js
1 1 import { Documents } from '../documents';
2 2  
3   -Meteor.publish( 'documents', function() {
4   - return Documents.find();
5   -});
  3 +Meteor.publish('documents', () => Documents.find());
... ...
imports/modules/get-input-value.js
1   -export const getInputValue = ( component, ref, nested ) => {
2   - let element = component.refs[ ref ];
  1 +export const getInputValue = (component, ref, nested) => {
  2 + const element = component.refs[ref];
3 3 return nested ? element.refs.input.value : element.value;
4   -}
  4 +};
... ...
imports/modules/login.js
... ... @@ -4,44 +4,44 @@ let component;
4 4  
5 5 const _handleLogin = () => {
6 6 // <Input /> component value is accessed via nested refs.
7   - const email = component.refs.emailAddress.refs.input.value,
8   - password = component.refs.password.value;
  7 + const email = component.refs.emailAddress.refs.input.value;
  8 + const password = component.refs.password.value;
9 9  
10   - Meteor.loginWithPassword( email, password, ( error ) => {
11   - if ( error ) {
12   - Bert.alert( error.reason, 'warning' );
  10 + Meteor.loginWithPassword(email, password, (error) => {
  11 + if (error) {
  12 + Bert.alert(error.reason, 'warning');
13 13 } else {
14   - browserHistory.push( '/' );
15   - Bert.alert( 'Logged in!', 'success' );
  14 + browserHistory.push('/');
  15 + Bert.alert('Logged in!', 'success');
16 16 }
17 17 });
18 18 };
19 19  
20 20 const _validate = () => {
21   - $( component.refs.login ).validate({
  21 + $(component.refs.login).validate({
22 22 rules: {
23 23 emailAddress: {
24 24 required: true,
25   - email: true
  25 + email: true,
26 26 },
27 27 password: {
28   - required: true
29   - }
  28 + required: true,
  29 + },
30 30 },
31 31 messages: {
32 32 emailAddress: {
33 33 required: 'Need an email address here.',
34   - email: 'Is this email address legit?'
  34 + email: 'Is this email address legit?',
35 35 },
36 36 password: {
37   - required: 'Need a password here.'
38   - }
  37 + required: 'Need a password here.',
  38 + },
39 39 },
40   - submitHandler() { _handleLogin(); }
  40 + submitHandler() { _handleLogin(); },
41 41 });
42 42 };
43 43  
44   -export const handleLogin = ( options ) => {
  44 +export const handleLogin = (options) => {
45 45 component = options.component;
46 46 _validate();
47 47 };
... ...
imports/modules/recover-password.js
... ... @@ -4,35 +4,35 @@ let component;
4 4  
5 5 const _handleRecovery = () => {
6 6 Accounts.forgotPassword({
7   - email: getInputValue( component, 'emailAddress', true )
8   - }, ( error ) => {
9   - if ( error ) {
10   - Bert.alert( error.reason, 'warning' );
  7 + email: getInputValue(component, 'emailAddress', true),
  8 + }, (error) => {
  9 + if (error) {
  10 + Bert.alert(error.reason, 'warning');
11 11 } else {
12   - Bert.alert( 'Check your inbox for a reset link!', 'success' );
  12 + Bert.alert('Check your inbox for a reset link!', 'success');
13 13 }
14 14 });
15 15 };
16 16  
17 17 const _validate = () => {
18   - $( component.refs.recoverPassword ).validate({
  18 + $(component.refs.recoverPassword).validate({
19 19 rules: {
20 20 emailAddress: {
21 21 required: true,
22   - email: true
23   - }
  22 + email: true,
  23 + },
24 24 },
25 25 messages: {
26 26 emailAddress: {
27 27 required: 'Need an email address here.',
28   - email: 'Is this email address legit?'
29   - }
  28 + email: 'Is this email address legit?',
  29 + },
30 30 },
31   - submitHandler() { _handleRecovery(); }
  31 + submitHandler() { _handleRecovery(); },
32 32 });
33 33 };
34 34  
35   -export const handleRecoverPassword = ( options ) => {
  35 +export const handleRecoverPassword = (options) => {
36 36 component = options.component;
37 37 _validate();
38 38 };
... ...
imports/modules/reset-password.js
1 1 import { getInputValue } from './get-input-value';
2 2 import { browserHistory } from 'react-router';
3 3  
4   -let component,
5   - token;
  4 +let component;
  5 +let token;
6 6  
7 7 const _handleReset = () => {
8   - const password = getInputValue( component, 'newPassword', true );
9   - Accounts.resetPassword( token, password, ( error ) => {
10   - if ( error ) {
11   - Bert.alert( error.reason, 'danger' );
  8 + const password = getInputValue(component, 'newPassword', true);
  9 + Accounts.resetPassword(token, password, (error) => {
  10 + if (error) {
  11 + Bert.alert(error.reason, 'danger');
12 12 } else {
13   - browserHistory.push( '/' );
14   - Bert.alert( 'Password reset!', 'success' );
  13 + browserHistory.push('/');
  14 + Bert.alert('Password reset!', 'success');
15 15 }
16 16 });
17 17 };
18 18  
19 19 const _validate = () => {
20   - $( component.refs.resetPassword ).validate({
  20 + $(component.refs.resetPassword).validate({
21 21 rules: {
22 22 newPassword: {
23 23 required: true,
24   - minlength: 6
  24 + minlength: 6,
25 25 },
26 26 repeatNewPassword: {
27 27 required: true,
28 28 minlength: 6,
29   - equalTo: '[name="newPassword"]'
30   - }
  29 + equalTo: '[name="newPassword"]',
  30 + },
31 31 },
32 32 messages: {
33 33 newPassword: {
34   - required: "Enter a new password, please.",
35   - minlength: "Use at least six characters, please."
  34 + required: 'Enter a new password, please.',
  35 + minlength: 'Use at least six characters, please.',
36 36 },
37 37 repeatNewPassword: {
38   - required: "Repeat your new password, please.",
39   - equalTo: "Hmm, your passwords don't match. Try again?"
40   - }
  38 + required: 'Repeat your new password, please.',
  39 + equalTo: 'Hmm, your passwords don\'t match. Try again?',
  40 + },
41 41 },
42   - submitHandler() { _handleReset(); }
  42 + submitHandler() { _handleReset(); },
43 43 });
44 44 };
45 45  
46   -export const handleResetPassword = ( options ) => {
  46 +export const handleResetPassword = (options) => {
47 47 component = options.component;
48   - token = options.token;
  48 + token = options.token;
49 49 _validate();
50 50 };
... ...
imports/modules/signup.js
... ... @@ -3,71 +3,69 @@ import { getInputValue } from &#39;./get-input-value&#39;;
3 3  
4 4 let component;
5 5  
6   -const _getUserData = () => {
7   - return {
8   - email: getInputValue( component, 'emailAddress', true ),
9   - password: getInputValue( component, 'password', true ),
10   - profile: {
11   - name: {
12   - first: getInputValue( component, 'firstName', true ),
13   - last: getInputValue( component, 'lastName', true )
14   - }
15   - }
16   - };
17   -};
  6 +const _getUserData = () => ({
  7 + email: getInputValue(component, 'emailAddress', true),
  8 + password: getInputValue(component, 'password', true),
  9 + profile: {
  10 + name: {
  11 + first: getInputValue(component, 'firstName', true),
  12 + last: getInputValue(component, 'lastName', true),
  13 + },
  14 + },
  15 +});
18 16  
19 17 const _handleSignup = () => {
20 18 const user = _getUserData();
21 19  
22   - Accounts.createUser( user, ( error ) => {
23   - if ( error ) {
24   - Bert.alert( error.reason, 'danger' );
  20 + Accounts.createUser(user, (error) => {
  21 + if (error) {
  22 + Bert.alert(error.reason, 'danger');
25 23 } else {
26   - browserHistory.push( '/' );
27   - Bert.alert( 'Welcome!', 'success' );
  24 + browserHistory.push('/');
  25 + Bert.alert('Welcome!', 'success');
28 26 }
29 27 });
30 28 };
31 29  
32 30 const _validate = () => {
33   - $( component.refs.signup ).validate({
  31 + $(component.refs.signup).validate({
34 32 rules: {
35 33 firstName: {
36   - required: true
  34 + required: true,
37 35 },
38 36 lastName: {
39   - required: true
  37 + required: true,
40 38 },
41 39 emailAddress: {
42 40 required: true,
43   - email: true
  41 + email: true,
44 42 },
45 43 password: {
46 44 required: true,
47   - minlength: 6
48   - }
  45 + minlength: 6,
  46 + },
49 47 },
50 48 messages: {
51 49 firstName: {
52   - required: 'First name?'
  50 + required: 'First name?',
53 51 },
54 52 lastName: {
55   - required: 'Last name?'
  53 + required: 'Last name?',
56 54 },
57 55 emailAddress: {
58 56 required: 'Need an email address here.',
59   - email: 'Is this email address legit?'
  57 + email: 'Is this email address legit?',
60 58 },
61 59 password: {
62 60 required: 'Need a password here.',
63   - minlength: 'Use at least six characters, please.'
64   - }
  61 + minlength: 'Use at least six characters, please.',
  62 + },
65 63 },
66   - submitHandler() { _handleSignup(); }
  64 + submitHandler() { _handleSignup(); },
67 65 });
68 66 };
69 67  
70   -export const handleSignup = ( options ) => {
  68 +export const handleSignup = (options) => {
71 69 component = options.component;
72 70 _validate();
73 71 };
... ...
imports/startup/client/index.js
1   -import './routes.jsx';
  1 +import './routes.js';
2 2  
3 3 Bert.defaults.style = 'growl-top-right';
... ...
imports/startup/client/routes.js
... ... @@ -0,0 +1,38 @@
  1 +import React from 'react';
  2 +import { render } from 'react-dom';
  3 +import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router';
  4 +
  5 +import { App } from '../../ui/layouts/app';
  6 +import { Documents } from '../../ui/pages/documents';
  7 +import { Index } from '../../ui/pages/index';
  8 +import { Login } from '../../ui/pages/login';
  9 +import { NotFound } from '../../ui/pages/not-found';
  10 +import { RecoverPassword } from '../../ui/pages/recover-password';
  11 +import { ResetPassword } from '../../ui/pages/reset-password';
  12 +import { Signup } from '../../ui/pages/signup';
  13 +
  14 +const requireAuth = (nextState, replace) => {
  15 + if (!Meteor.loggingIn() && !Meteor.user()) {
  16 + replace({
  17 + pathname: '/login',
  18 + state: { nextPathName: nextState.location.pathname },
  19 + });
  20 + }
  21 +};
  22 +
  23 +Meteor.startup(() => {
  24 + render(
  25 + <Router history={ browserHistory }>
  26 + <Route path="/" component={ App }>
  27 + <IndexRoute name="index" component={ Index } onEnter={ requireAuth } />
  28 + <Route name="documents" path="/documents" component={ Documents } onEnter={ requireAuth } />
  29 + <Route name="login" path="/login" component={ Login } />
  30 + <Route name="recover-password" path="/recover-password" component={ RecoverPassword } />
  31 + <Route name="reset-password" path="/reset-password/:token" component={ ResetPassword } />
  32 + <Route name="signup" path="/signup" component={ Signup } />
  33 + <Route path="*" component={ NotFound } />
  34 + </Route>
  35 + </Router>,
  36 + document.getElementById('react-root')
  37 + );
  38 +});
... ...
imports/startup/client/routes.jsx
... ... @@ -1,38 +0,0 @@
1   -import React from 'react';
2   -import { render } from 'react-dom';
3   -import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router';
4   -
5   -import { App } from '../../ui/layouts/app';
6   -import { Documents } from '../../ui/pages/documents';
7   -import { Index } from '../../ui/pages/index';
8   -import { Login } from '../../ui/pages/login';
9   -import { NotFound } from '../../ui/pages/not-found';
10   -import { RecoverPassword } from '../../ui/pages/recover-password';
11   -import { ResetPassword } from '../../ui/pages/reset-password';
12   -import { Signup } from '../../ui/pages/signup';
13   -
14   -const requireAuth = ( nextState, replace ) => {
15   - if ( !Meteor.loggingIn() && !Meteor.user() ) {
16   - replace({
17   - pathname: '/login',
18   - state: { nextPathName: nextState.location.pathname }
19   - });
20   - }
21   -};
22   -
23   -Meteor.startup( () => {
24   - render(
25   - <Router history={ browserHistory }>
26   - <Route path="/" component={ App }>
27   - <IndexRoute name="index" component={ Index } onEnter={ requireAuth } />
28   - <Route name="documents" path="/documents" component={ Documents } onEnter={ requireAuth } />
29   - <Route name="login" path="/login" component={ Login } />
30   - <Route name="recover-password" path="/recover-password" component={ RecoverPassword } />
31   - <Route name="reset-password" path="/reset-password/:token" component={ ResetPassword } />
32   - <Route name="signup" path="/signup" component={ Signup } />
33   - <Route path="*" component={ NotFound } />
34   - </Route>
35   - </Router>,
36   - document.getElementById( 'react-root' )
37   - );
38   -});
imports/startup/server/accounts/email-templates.js
1   -const name = 'Application Name',
2   - email = '<support@application.com>',
3   - from = `${ name } ${ email }`,
4   - emailTemplates = Accounts.emailTemplates;
  1 +const name = 'Application Name';
  2 +const email = '<support@application.com>';
  3 +const from = `${name} ${email}`;
  4 +const emailTemplates = Accounts.emailTemplates;
5 5  
6 6 emailTemplates.siteName = name;
7   -emailTemplates.from = from;
  7 +emailTemplates.from = from;
8 8  
9 9 emailTemplates.resetPassword = {
10 10 subject() {
11   - return `[${ name }] Reset Your Password`;
  11 + return `[${name}] Reset Your Password`;
12 12 },
13   - text( user, url ) {
14   - let userEmail = user.emails[ 0 ].address,
15   - urlWithoutHash = url.replace( '#/', '' );
  13 + text(user, url) {
  14 + const userEmail = user.emails[0].address;
  15 + const urlWithoutHash = url.replace('#/', '');
16 16  
17   - return `A password reset has been requested for the account related to this address (${ userEmail }). To reset the password, visit the following link:\n\n${ urlWithoutHash }\n\n If you did not request this reset, please ignore this email. If you feel something is wrong, please contact our support team: ${ email }.`;
18   - }
  17 + return `A password reset has been requested for the account related to this
  18 + address (${userEmail}). To reset the password, visit the following link:
  19 + \n\n${urlWithoutHash}\n\n If you did not request this reset, please ignore
  20 + this email. If you feel something is wrong, please contact our support team:
  21 + ${email}.`;
  22 + },
19 23 };
... ...
imports/startup/server/fixtures.js
... ... @@ -2,18 +2,14 @@ const users = [{
2 2 email: 'admin@admin.com',
3 3 password: 'password',
4 4 profile: {
5   - name: { first: 'Carl', last: 'Winslow' }
6   - }
  5 + name: { first: 'Carl', last: 'Winslow' },
  6 + },
7 7 }];
8 8  
9   -users.forEach( ( { email, password, profile } ) => {
10   - const userExists = Meteor.users.findOne( { 'emails.address': email } );
  9 +users.forEach(({ email, password, profile }) => {
  10 + const userExists = Meteor.users.findOne({ 'emails.address': email });
11 11  
12   - if ( !userExists ) {
13   - Accounts.createUser({
14   - email: email,
15   - password: password,
16   - profile: profile
17   - });
  12 + if (!userExists) {
  13 + Accounts.createUser({ email, password, profile });
18 14 }
19 15 });
... ...
imports/ui/components/add-document.js
... ... @@ -2,19 +2,19 @@ import React from &#39;react&#39;;
2 2 import { Row, Col, ListGroup, ListGroupItem, Input, Alert } from 'react-bootstrap';
3 3 import { insertDocument } from '../../api/documents/methods.js';
4 4  
5   -const handleInsertDocument = ( event ) => {
6   - const target = event.target,
7   - title = target.value.trim();
  5 +const handleInsertDocument = (event) => {
  6 + const target = event.target;
  7 + const title = target.value.trim();
8 8  
9   - if ( title !== '' && event.keyCode === 13 ) {
  9 + if (title !== '' && event.keyCode === 13) {
10 10 insertDocument.call({
11   - title: title
12   - }, ( error, response ) => {
13   - if ( error ) {
14   - Bert.alert( error.reason, 'danger' );
  11 + title,
  12 + }, (error) => {
  13 + if (error) {
  14 + Bert.alert(error.reason, 'danger');
15 15 } else {
16 16 target.value = '';
17   - Bert.alert( 'Document added!', 'success' );
  17 + Bert.alert('Document added!', 'success');
18 18 }
19 19 });
20 20 }
... ... @@ -26,4 +26,4 @@ export const AddDocument = () =&gt; (
26 26 onKeyUp={ handleInsertDocument }
27 27 placeholder="Type a document title and press enter..."
28 28 />
29   -)
  29 +);
... ...
imports/ui/components/app-navigation.js
... ... @@ -5,7 +5,7 @@ import { PublicNavigation } from &#39;./public-navigation&#39;;
5 5 import { AuthenticatedNavigation } from './authenticated-navigation';
6 6  
7 7 export class AppNavigation extends React.Component {
8   - renderNavigation( hasUser ) {
  8 + renderNavigation(hasUser) {
9 9 return hasUser ? <AuthenticatedNavigation /> : <PublicNavigation />;
10 10 }
11 11  
... ... @@ -18,7 +18,7 @@ export class AppNavigation extends React.Component {
18 18 <Navbar.Toggle />
19 19 </Navbar.Header>
20 20 <Navbar.Collapse>
21   - { this.renderNavigation( this.props.hasUser ) }
  21 + { this.renderNavigation(this.props.hasUser) }
22 22 </Navbar.Collapse>
23 23 </Navbar>;
24 24 }
... ...
imports/ui/components/authenticated-navigation.js
... ... @@ -3,16 +3,12 @@ import { browserHistory } from &#39;react-router&#39;;
3 3 import { IndexLinkContainer, LinkContainer } from 'react-router-bootstrap';
4 4 import { Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap';
5 5  
6   -const handleLogout = () => {
7   - return Meteor.logout( () => browserHistory.push( '/login' ) );
8   -};
  6 +const handleLogout = () => Meteor.logout(() => browserHistory.push('/login'));
9 7  
10 8 const userName = () => {
11 9 const user = Meteor.user();
12   - if ( user ) {
13   - const name = user && user.profile ? user.profile.name : '';
14   - return `${ name.first } ${ name.last }`;
15   - }
  10 + const name = user && user.profile ? user.profile.name : '';
  11 + return user ? `${name.first} ${name.last}` : '';
16 12 };
17 13  
18 14 export const AuthenticatedNavigation = () => (
... ... @@ -31,4 +27,4 @@ export const AuthenticatedNavigation = () =&gt; (
31 27 </NavDropdown>
32 28 </Nav>
33 29 </div>
34   -)
  30 +);
... ...
imports/ui/components/document.js
... ... @@ -0,0 +1,57 @@
  1 +import React from 'react';
  2 +import { Row, Col, ListGroupItem, Input, Button } from 'react-bootstrap';
  3 +import { updateDocument, removeDocument } from '../../api/documents/methods.js';
  4 +
  5 +const handleUpdateDocument = (documentId, event) => {
  6 + const title = event.target.value.trim();
  7 + if (title !== '' && event.keyCode === 13) {
  8 + updateDocument.call({
  9 + _id: documentId,
  10 + update: { title },
  11 + }, (error) => {
  12 + if (error) {
  13 + Bert.alert(error.reason, 'danger');
  14 + } else {
  15 + Bert.alert('Document updated!', 'success');
  16 + }
  17 + });
  18 + }
  19 +};
  20 +
  21 +const handleRemoveDocument = (documentId, event) => {
  22 + event.preventDefault();
  23 + if (confirm('Are you sure? This is permanent.')) {
  24 + removeDocument.call({
  25 + _id: documentId,
  26 + }, (error) => {
  27 + if (error) {
  28 + Bert.alert(error.reason, 'danger');
  29 + } else {
  30 + Bert.alert('Document removed!', 'success');
  31 + }
  32 + });
  33 + }
  34 +};
  35 +
  36 +export const Document = ({ document }) => (
  37 + <ListGroupItem key={ document._id }>
  38 + <Row>
  39 + <Col xs={ 8 } sm={ 10 }>
  40 + <Input
  41 + type="text"
  42 + standalone
  43 + defaultValue={ document.title }
  44 + onKeyUp={ handleUpdateDocument.bind(this, document._id) }
  45 + />
  46 + </Col>
  47 + <Col xs={ 4 } sm={ 2 }>
  48 + <Button
  49 + bsStyle="danger"
  50 + className="btn-block"
  51 + onClick={ handleRemoveDocument.bind(this, document._id) }>
  52 + Remove
  53 + </Button>
  54 + </Col>
  55 + </Row>
  56 + </ListGroupItem>
  57 +);
... ...
imports/ui/components/documents-list.js
1 1 import React from 'react';
2   -import { Row, Col, ListGroup, ListGroupItem, Input, Button, Alert } from 'react-bootstrap';
3   -import { updateDocument, removeDocument } from '../../api/documents/methods.js';
  2 +import { Row, Col, ListGroup, Alert } from 'react-bootstrap';
  3 +import { Document } from './document.js';
4 4  
5   -const handleUpdateDocument = ( documentId, event ) => {
6   - const title = event.target.value.trim();
7   - if ( title !== '' && event.keyCode === 13 ) {
8   - updateDocument.call({
9   - _id: documentId,
10   - update: { title: title }
11   - }, ( error, response ) => {
12   - if ( error ) {
13   - Bert.alert( error.reason, 'danger' );
14   - }
15   - });
16   - }
17   -};
18   -
19   -const handleRemoveDocument = ( documentId, event ) => {
20   - event.preventDefault();
21   - if ( confirm( 'Are you sure? This is permanent.' ) ) {
22   - removeDocument.call({
23   - _id: documentId
24   - }, ( error, response ) => {
25   - if ( error ) {
26   - Bert.alert( error.reason, 'danger' );
27   - } else {
28   - Bert.alert( 'Document removed!', 'success' );
29   - }
30   - });
31   - }
32   -};
33   -
34   -export const DocumentsList = ( { documents } ) => {
35   - if ( documents.length > 0 ) {
36   - return <ListGroup className="documents-list">
37   - {documents.map( ( { _id, title } ) => {
38   - return <ListGroupItem key={ _id }>
39   - <Row>
40   - <Col xs={ 8 } sm={ 10 }>
41   - <Input
42   - type="text"
43   - standalone
44   - defaultValue={ title }
45   - onKeyUp={ handleUpdateDocument.bind( this, _id ) }
46   - />
47   - </Col>
48   - <Col xs={ 4 } sm={ 2 }>
49   - <Button
50   - bsStyle="danger"
51   - className="btn-block"
52   - onClick={ handleRemoveDocument.bind( this, _id ) }>
53   - Remove
54   - </Button>
55   - </Col>
56   - </Row>
57   - </ListGroupItem>;
58   - })}
59   - </ListGroup>;
60   - } else {
61   - return <Alert bsStyle="warning">No documents yet.</Alert>;
62   - }
63   -};
  5 +export const DocumentsList = ({ documents }) => (
  6 + documents.length > 0 ? <ListGroup className="documents-list">
  7 + {documents.map((doc) => (
  8 + <Document key={ doc._id } document={ doc } />
  9 + ))}
  10 + </ListGroup> :
  11 + <Alert bsStyle="warning">No documents yet.</Alert>
  12 +);
... ...
imports/ui/components/public-navigation.js
... ... @@ -11,4 +11,4 @@ export const PublicNavigation = () =&gt; (
11 11 <NavItem eventKey={ 2 } href="/login">Log In</NavItem>
12 12 </LinkContainer>
13 13 </Nav>
14   -)
  14 +);
... ...
imports/ui/containers/app-navigation.js
1 1 import { composeWithTracker } from 'react-komposer';
2 2 import { AppNavigation } from '../components/app-navigation';
3 3  
4   -const composer = ( props, onData ) => {
5   - onData( null, { hasUser: Meteor.user() } );
  4 +const composer = (props, onData) => {
  5 + onData(null, { hasUser: Meteor.user() });
6 6 };
7 7  
8   -export default composeWithTracker( composer, {}, {}, { pure: false } )( AppNavigation );
  8 +export default composeWithTracker(composer, {}, {}, { pure: false })(AppNavigation);
... ...
imports/ui/containers/documents-list.js
... ... @@ -3,12 +3,12 @@ import { composeWithTracker } from &#39;react-komposer&#39;;
3 3 import { Documents } from '../../api/documents/documents.js';
4 4 import { DocumentsList } from '../components/documents-list.js';
5 5  
6   -const composer = ( params, onReady ) => {
7   - const subscription = Meteor.subscribe( 'documents' );
8   - if ( subscription.ready() ) {
9   - let documents = Documents.find().fetch();
10   - onReady( null, { documents } );
  6 +const composer = (params, onReady) => {
  7 + const subscription = Meteor.subscribe('documents');
  8 + if (subscription.ready()) {
  9 + const documents = Documents.find().fetch();
  10 + onReady(null, { documents });
11 11 }
12 12 };
13 13  
14   -export default composeWithTracker( composer )( DocumentsList );
  14 +export default composeWithTracker(composer)(DocumentsList);
... ...
imports/ui/layouts/app.js
... ... @@ -6,7 +6,7 @@ export const App = React.createClass({
6 6 contextTypes: {
7 7 router() {
8 8 return React.PropTypes.func.isRequired;
9   - }
  9 + },
10 10 },
11 11 render() {
12 12 const { isActive } = this.context.router;
... ... @@ -16,5 +16,5 @@ export const App = React.createClass({
16 16 { this.props.children }
17 17 </Grid>
18 18 </div>;
19   - }
  19 + },
20 20 });
... ...
imports/ui/pages/documents.js
... ... @@ -11,4 +11,4 @@ export const Documents = () =&gt; (
11 11 <DocumentsList />
12 12 </Col>
13 13 </Row>
14   -)
  14 +);
... ...
imports/ui/pages/index.js
... ... @@ -8,4 +8,4 @@ export const Index = () =&gt; (
8 8 <p><a className="btn btn-success" href="https://themeteorchef.com/base" role="button">Read the Documentation</a></p>
9 9 <p style={ { fontSize: '16px', color: '#aaa' } }>Currently at v4.0.0</p>
10 10 </Jumbotron>
11   -)
  11 +);
... ...
imports/ui/pages/login.js
... ... @@ -5,10 +5,10 @@ import { handleLogin } from &#39;../../modules/login&#39;;
5 5  
6 6 export class Login extends React.Component {
7 7 componentDidMount() {
8   - handleLogin( { component: this } );
  8 + handleLogin({ component: this });
9 9 }
10 10  
11   - handleSubmit( event ) {
  11 + handleSubmit(event) {
12 12 event.preventDefault();
13 13 }
14 14  
... ... @@ -29,7 +29,13 @@ export class Login extends React.Component {
29 29 <span className="pull-left">Password</span>
30 30 <Link className="pull-right" to="/recover-password">Forgot Password?</Link>
31 31 </label>
32   - <input type="password" className="form-control" ref="password" name="password" placeholder="Password" />
  32 + <input
  33 + type="password"
  34 + className="form-control"
  35 + ref="password"
  36 + name="password"
  37 + placeholder="Password"
  38 + />
33 39 </div>
34 40 <Button type="submit" bsStyle="success">Login</Button>
35 41 </form>
... ...
imports/ui/pages/not-found.js
... ... @@ -6,4 +6,4 @@ export const NotFound = () =&gt; (
6 6 <Alert bsStyle="danger">
7 7 <p><strong>Error [404]</strong>: { window.location.pathname } does not exist.</p>
8 8 </Alert>
9   -)
  9 +);
... ...
imports/ui/pages/recover-password.js
... ... @@ -4,10 +4,10 @@ import { handleRecoverPassword } from &#39;../../modules/recover-password&#39;;
4 4  
5 5 export class RecoverPassword extends React.Component {
6 6 componentDidMount() {
7   - handleRecoverPassword( { component: this } );
  7 + handleRecoverPassword({ component: this });
8 8 }
9 9  
10   - handleSubmit( event ) {
  10 + handleSubmit(event) {
11 11 event.preventDefault();
12 12 }
13 13  
... ... @@ -15,7 +15,9 @@ export class RecoverPassword extends React.Component {
15 15 return <Row>
16 16 <Col xs={ 12 } sm={ 6 } md={ 4 }>
17 17 <h4 className="page-header">Recover Password</h4>
18   - <Alert bsStyle="info">Enter your email address below to receive a link to reset your password.</Alert>
  18 + <Alert bsStyle="info">
  19 + Enter your email address below to receive a link to reset your password.
  20 + </Alert>
19 21 <form ref="recoverPassword" className="recover-password" onSubmit={ this.handleSubmit }>
20 22 <Input
21 23 type="email"
... ...
imports/ui/pages/reset-password.js
... ... @@ -6,11 +6,11 @@ export class ResetPassword extends React.Component {
6 6 componentDidMount() {
7 7 handleResetPassword({
8 8 component: this,
9   - token: this.props.params.token
  9 + token: this.props.params.token,
10 10 });
11 11 }
12 12  
13   - handleSubmit( event ) {
  13 + handleSubmit(event) {
14 14 event.preventDefault();
15 15 }
16 16  
... ... @@ -18,7 +18,10 @@ export class ResetPassword extends React.Component {
18 18 return <Row>
19 19 <Col xs={ 12 } sm={ 6 } md={ 4 }>
20 20 <h4 className="page-header">Reset Password</h4>
21   - <Alert bsStyle="info">To reset your password, enter a new one below. You will be logged in with your new password.</Alert>
  21 + <Alert bsStyle="info">
  22 + To reset your password, enter a new one below. You will be logged in
  23 +with your new password.
  24 + </Alert>
22 25 <form ref="resetPassword" className="reset-password" onSubmit={ this.handleSubmit }>
23 26 <Input
24 27 label="New Password"
... ...
imports/ui/pages/signup.js
... ... @@ -5,10 +5,10 @@ import { handleSignup } from &#39;../../modules/signup&#39;;
5 5  
6 6 export class Signup extends React.Component {
7 7 componentDidMount() {
8   - handleSignup( { component: this } );
  8 + handleSignup({ component: this });
9 9 }
10 10  
11   - handleSubmit( event ) {
  11 + handleSubmit(event) {
12 12 event.preventDefault();
13 13 }
14 14  
... ...
... ... @@ -10,12 +10,47 @@
10 10 "production": "meteor deploy production.meteor.com --settings settings-production.json"
11 11 },
12 12 "devDependencies": {},
13   - "dependencies": {
  13 + "eslintConfig": {
  14 + "parserOptions": {
  15 + "ecmaFeatures": {
  16 + "jsx": true
  17 + }
  18 + },
  19 + "plugins": [
  20 + "meteor",
  21 + "react"
  22 + ],
  23 + "extends": [
  24 + "airbnb/base",
  25 + "plugin:meteor/guide"
  26 + ],
  27 + "globals": {
  28 + "$": false,
  29 + "Accounts": false,
  30 + "Bert": false,
  31 + "browser": false,
  32 + "expect": false,
  33 + "Factory": false,
  34 + "Meteor": false,
  35 + "server": false,
  36 + "SimpleSchema": false,
  37 + "ValidatedMethod": false
  38 + },
  39 + "rules": {}
  40 + },
  41 + "devDependencies": {
14 42 "chimp": "^0.33.0",
15   - "react": "^0.14.8",
16   - "react-addons-pure-render-mixin": "^0.14.8",
  43 + "eslint": "^2.6.0",
  44 + "eslint-config-airbnb": "^6.2.0",
  45 + "eslint-plugin-meteor": "^3.4.0",
  46 + "eslint-plugin-react": "^4.2.3",
  47 + "faker": "^3.1.0"
  48 + },
  49 + "dependencies": {
  50 + "react": "^15.0.1",
  51 + "react-addons-pure-render-mixin": "^15.0.1",
17 52 "react-bootstrap": "^0.28.4",
18   - "react-dom": "^0.14.7",
  53 + "react-dom": "^15.0.1",
19 54 "react-komposer": "^1.7.1",
20 55 "react-router": "^2.0.1",
21 56 "react-router-bootstrap": "^0.20.1"
... ...
1   -describe( 'Log In', function() {
2   - beforeEach( function() {
3   - server.execute( function() {
4   - var user = Meteor.users.findOne( { 'emails.address': 'carl.winslow@abc.com' } );
5   - if ( user ) {
6   - Meteor.users.remove( user._id );
  1 +/* eslint-env mocha */
  2 +/* eslint-disable func-names, prefer-arrow-callback */
  3 +
  4 +describe('Log In', function () {
  5 + beforeEach(function () {
  6 + server.execute(function () {
  7 + const user = Meteor.users.findOne({ 'emails.address': 'carl.winslow@abc.com' });
  8 + if (user) {
  9 + Meteor.users.remove(user._id);
7 10 }
8 11 });
9 12 });
10 13  
11   - it( 'should allow us to login', function() {
12   - server.execute( function() {
  14 + it('should allow us to login', function () {
  15 + server.execute(function () {
13 16 Accounts.createUser({
14 17 email: 'carl.winslow@abc.com',
15 18 password: 'bigguy1989',
16 19 profile: {
17   - name: { first: 'Carl', last: 'Winslow' }
18   - }
  20 + name: { first: 'Carl', last: 'Winslow' },
  21 + },
19 22 });
20 23 });
21 24  
22   - browser.url( 'http://localhost:3000/login' )
23   - .setValue( '[name="emailAddress"]', 'carl.winslow@abc.com' )
24   - .setValue( '[name="password"]', 'bigguy1989' )
25   - .submitForm( 'form' );
  25 + browser.url('http://localhost:3000/login')
  26 + .setValue('[name="emailAddress"]', 'carl.winslow@abc.com')
  27 + .setValue('[name="password"]', 'bigguy1989')
  28 + .submitForm('form');
26 29  
27   - browser.waitForExist( '.jumbotron' );
28   - expect( browser.getUrl() ).to.equal( 'http://localhost:3000/' );
  30 + browser.waitForExist('.jumbotron');
  31 + expect(browser.getUrl()).to.equal('http://localhost:3000/');
29 32 });
30 33 });
... ...
tests/not-found.js
1   -describe( '404 Error', function() {
2   - it( 'should render a 404 for a non-existent route', function() {
3   - browser.url( 'http://localhost:3000/dididothat' )
4   - .waitForExist( '.alert-danger' );
  1 +/* eslint-env mocha */
  2 +/* eslint-disable func-names, prefer-arrow-callback */
5 3  
6   - expect( browser.getText( '.alert-danger p' ) ).to.equal( 'Error [404]: /dididothat does not exist.' );
  4 +describe('404 Error', function () {
  5 + it('should render a 404 for a non-existent route', function () {
  6 + browser.url('http://localhost:3000/dididothat')
  7 + .waitForExist('.alert-danger');
  8 +
  9 + expect(browser.getText('.alert-danger p')).to.equal('Error [404]: /dididothat does not exist.');
7 10 });
8 11 });
... ...
tests/reset-password.js
... ... @@ -1,34 +0,0 @@
1   -describe( 'Reset Password', function() {
2   - beforeEach( function() {
3   - server.execute( function() {
4   - var user = Meteor.users.findOne( { 'emails.address': 'carl.winslow@abc.com' } );
5   - if ( user ) { Meteor.users.remove( user._id ); }
6   -
7   - Accounts.createUser({
8   - email: 'carl.winslow@abc.com',
9   - password: 'bigguy1989',
10   - profile: {
11   - name: { first: 'Carl', last: 'Winslow' }
12   - }
13   - });
14   - });
15   - });
16   -
17   - it( 'should send a recovery email when we request a reset @watch', function() {
18   - browser.url( 'http://localhost:3000/recover-password' )
19   - .setValue( '[name="emailAddress"]', 'carl.winslow@abc.com' )
20   - .submitForm( 'form' );
21   -
22   - let emails = server.call( 'emailStub/getEmails' );
23   - console.log( emails[0] ); // Why no value?
24   -
25   - // browser.url( 'http://localhost:3000' );
26   - // browser.execute( function() {
27   - // Accounts.onResetPasswordLink( function( token, finished ) {
28   - // expect( token ).to.equal( 'blah' );
29   - // finished();
30   - // done();
31   - // });
32   - // });
33   - });
34   -});
1   -describe( 'Sign Up', function() {
2   - beforeEach( function() {
3   - server.execute( function() {
4   - var user = Meteor.users.findOne( { 'emails.address': 'carl.winslow@abc.com' } );
5   - if ( user ) {
6   - Meteor.users.remove( user._id );
  1 +/* eslint-env mocha */
  2 +/* eslint-disable func-names, prefer-arrow-callback */
  3 +
  4 +describe('Sign Up', function () {
  5 + beforeEach(function () {
  6 + server.execute(function () {
  7 + const user = Meteor.users.findOne({ 'emails.address': 'carl.winslow@abc.com' });
  8 + if (user) {
  9 + Meteor.users.remove(user._id);
7 10 }
8 11 });
9 12 });
10 13  
11   - it( 'should create a new user and login with redirect to index', function() {
12   - browser.url( 'http://localhost:3000/signup' )
13   - .setValue( '[name="firstName"]', 'Carl' )
14   - .setValue( '[name="lastName"]', 'Winslow' )
15   - .setValue( '[name="emailAddress"]', 'carl.winslow@abc.com' )
16   - .setValue( '[name="password"]', 'bigguy1989' )
17   - .submitForm( 'form' );
  14 + it('should create a new user and login with redirect to index', function () {
  15 + browser.url('http://localhost:3000/signup')
  16 + .setValue('[name="firstName"]', 'Carl')
  17 + .setValue('[name="lastName"]', 'Winslow')
  18 + .setValue('[name="emailAddress"]', 'carl.winslow@abc.com')
  19 + .setValue('[name="password"]', 'bigguy1989')
  20 + .submitForm('form');
18 21  
19   - browser.waitForExist( '.jumbotron' );
20   - expect( browser.getUrl() ).to.equal( 'http://localhost:3000/' );
  22 + browser.waitForExist('.jumbotron');
  23 + expect(browser.getUrl()).to.equal('http://localhost:3000/');
21 24 });
22 25 });
... ...