├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── README.md ├── client ├── helpers │ └── error.js ├── main.html ├── stylesheets │ └── style.css └── templates │ ├── application │ └── not_found.jsx │ ├── comments │ ├── comment_item.jsx │ └── comment_submit.jsx │ ├── includes │ ├── access_denied.jsx │ ├── errors.jsx │ ├── header.jsx │ └── loading.jsx │ └── posts │ ├── post_edit.jsx │ ├── post_item.jsx │ ├── post_list.jsx │ ├── post_page.jsx │ └── post_submit.jsx ├── lib ├── collections │ ├── comments.jsx │ └── post.jsx ├── permissions.js └── routes │ └── routers.jsx ├── server ├── fixtures.js ├── publications.js └── userReg.js └── tests ├── cucumber ├── features │ ├── authentication │ │ ├── authentication.feature │ │ └── authentication.js │ ├── navigation │ │ ├── navigate_steps.js │ │ └── navigation.feature │ ├── posts │ │ └── submit_new_post │ │ │ ├── submit_new_post.feature │ │ │ └── submit_new_post.js │ ├── static_pages │ │ ├── static_pages.feature │ │ └── static_pages.js │ └── support │ │ └── hooks.js └── fixtures │ ├── posts.js │ └── users.js └── jasmine └── client ├── integration ├── components │ └── headerComponentSpec.js ├── fixtures │ └── users.js ├── spec_helper.js └── spec_helper_shallow.js └── unit ├── components ├── post_item_spec.js ├── post_list_spec.js └── post_submit_spec.js ├── spec_helper.js └── spec_helper_shallow.js /.gitignore: -------------------------------------------------------------------------------- 1 | reated by https://www.gitignore.io 2 | 3 | ### WebStorm ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 5 | 6 | *.iml 7 | 8 | ## Directory-based project format: 9 | .idea/ 10 | # if you remove the above rule, at least ignore the following: 11 | 12 | # User-specific stuff: 13 | .idea/workspace.xml 14 | .idea/tasks.xml 15 | .idea/dictionaries 16 | 17 | # Sensitive or high-churn files: 18 | # .idea/dataSources.ids 19 | # .idea/dataSources.xml 20 | # .idea/sqlDataSources.xml 21 | # .idea/dynamic.xml 22 | # .idea/uiDesigner.xml 23 | 24 | # Gradle: 25 | # .idea/gradle.xml 26 | # .idea/libraries 27 | 28 | # Mongo Explorer plugin: 29 | # .idea/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.ipr 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Crashlytics plugin (for Android Studio and IntelliJ) 47 | com_crashlytics_export_strings.xml 48 | crashlytics.properties 49 | crashlytics-build.properties 50 | 51 | 52 | ### Meteor ### 53 | .meteor/local 54 | 55 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1mi3qhh1s7sd2i1uwlvq 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | twbs:bootstrap 8 | 9 | accounts-password 10 | accounts-ui 11 | react 12 | xolvio:cucumber 13 | kadira:flow-router 14 | sanjo:jasmine 15 | standard-minifiers 16 | meteor-base 17 | mobile-experience 18 | mongo 19 | blaze-html-templates 20 | session 21 | jquery 22 | tracker 23 | logging 24 | reload 25 | check 26 | ecmascript 27 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.0.1 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.1 2 | accounts-password@1.1.3 3 | accounts-ui@1.1.6 4 | accounts-ui-unstyled@1.1.8 5 | amplify@1.0.0 6 | autoupdate@1.2.3 7 | babel-compiler@5.8.24_1 8 | babel-runtime@0.1.4 9 | base64@1.0.4 10 | binary-heap@1.0.4 11 | blaze@2.1.3 12 | blaze-html-templates@1.0.1 13 | blaze-tools@1.0.4 14 | boilerplate-generator@1.0.4 15 | caching-compiler@1.0.0 16 | caching-html-compiler@1.0.1 17 | callback-hook@1.0.4 18 | check@1.0.6 19 | coffeescript@1.0.9 20 | cosmos:browserify@0.7.0 21 | ddp@1.2.2 22 | ddp-client@1.2.1 23 | ddp-common@1.2.1 24 | ddp-rate-limiter@1.0.0 25 | ddp-server@1.2.1 26 | deps@1.0.9 27 | diff-sequence@1.0.1 28 | ecmascript@0.1.4 29 | ecmascript-collections@0.1.6 30 | ejson@1.0.7 31 | email@1.0.7 32 | fastclick@1.0.7 33 | geojson-utils@1.0.4 34 | hot-code-push@1.0.0 35 | html-tools@1.0.5 36 | htmljs@1.0.5 37 | http@1.1.1 38 | id-map@1.0.4 39 | jquery@1.11.4 40 | jsx@0.2.1 41 | kadira:flow-router@2.6.2 42 | launch-screen@1.0.4 43 | less@2.5.0_2 44 | livedata@1.0.15 45 | localstorage@1.0.5 46 | logging@1.0.8 47 | meteor@1.1.7 48 | meteor-base@1.0.1 49 | minifiers@1.1.7 50 | minimongo@1.0.9 51 | mobile-experience@1.0.1 52 | mobile-status-bar@1.0.6 53 | mongo@1.1.1 54 | mongo-id@1.0.1 55 | npm-bcrypt@0.7.8_2 56 | npm-mongo@1.4.39_1 57 | observe-sequence@1.0.7 58 | ordered-dict@1.0.4 59 | package-version-parser@3.0.4 60 | practicalmeteor:chai@2.1.0_1 61 | practicalmeteor:loglevel@1.2.0_2 62 | promise@0.4.8 63 | random@1.0.4 64 | rate-limit@1.0.0 65 | react@0.1.13 66 | react-meteor-data@0.1.9 67 | react-runtime@0.13.3_7 68 | react-runtime-dev@0.13.3_7 69 | react-runtime-prod@0.13.3_6 70 | reactive-dict@1.1.1 71 | reactive-var@1.0.6 72 | reload@1.1.4 73 | retry@1.0.4 74 | routepolicy@1.0.6 75 | sanjo:jasmine@0.19.0 76 | sanjo:karma@1.7.0 77 | sanjo:long-running-child-process@1.1.3 78 | sanjo:meteor-files-helpers@1.1.0_7 79 | sanjo:meteor-version@1.0.0 80 | service-configuration@1.0.5 81 | session@1.1.1 82 | sha@1.0.4 83 | simple:json-routes@1.0.4 84 | spacebars@1.0.7 85 | spacebars-compiler@1.0.7 86 | srp@1.0.4 87 | standard-minifiers@1.0.0 88 | templating@1.1.3 89 | templating-tools@1.0.0 90 | tracker@1.0.8 91 | twbs:bootstrap@3.3.5 92 | ui@1.0.8 93 | underscore@1.0.4 94 | url@1.0.5 95 | velocity:chokidar@1.0.3_1 96 | velocity:core@0.10.0 97 | velocity:html-reporter@0.9.0 98 | velocity:meteor-internals@1.1.0_7 99 | velocity:meteor-stubs@1.1.0 100 | velocity:shim@0.1.0 101 | velocity:source-map-support@0.3.2_1 102 | webapp@1.2.2 103 | webapp-hashing@1.0.5 104 | xolvio:cucumber@0.14.0 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microscope + React + FlowRouter + Tests = Microreact 2 | 3 | I wanted to learn React with Meteor by building something. So I decided to 4 | rebuild [Microscope] (https://github.com/DiscoverMeteor/Microscope), the app with which I learned Meteor. 5 | [Microscope] (https://github.com/DiscoverMeteor/Microscope) is an app that someone can build by reading the [Discover Meteor Book] (http://www.discovermeteor.com). 6 | In this version I rebuilt the app until chapter 10-5 and you can find a dome [here](http://microreact.meteor.com). You can find a dome at [Microreact](http://microreact.meteor.com/). 7 | 8 | Except from integrating React and Meteor I wanted to learn how to write tests for my applications. So in this repo you will find 60 tests, 13 unit tests, 6 integration and 41 end-to-end. 9 | I used [xolvio:cucumber] (https://atmospherejs.com/xolvio/cucumber) for end-to-end tests and [sanjo:jasmine](https://atmospherejs.com/sanjo/jasmine) for integration and unit tests. I tried to explain every line 10 | of code into my tests. 11 | 12 | If you want to run the app download the repo 13 | 14 | ``` 15 | $ git clone https://github.com/mstamos/microreact.git 16 | ``` 17 | 18 | ``` 19 | $ cd microreact 20 | ``` 21 | 22 | If you want to run only integration and unit test just run meteor into command line and you will see the green dot from velocity 23 | on right-top corner 24 | 25 | ``` 26 | $ meteor 27 | ``` 28 | 29 | If you want to run the end-to-end test you can do it by typing and enjoy the view from new browser! 30 | 31 | ``` 32 | $ CUCUMBER_TAGS=@rerun meteor 33 | ``` 34 | 35 | 36 | Below you will find all the resources I used to learn writing tests. 37 | 38 | ## Test Resources 39 | 40 | ###1. Cucumber 41 | #####1.1 Articles 42 | 1. [Announcing Xolv.io Cucumber v0.14 - Synchronous Testing!](https://forums.meteor.com/t/announcing-xolv-io-cucumber-v0-14-synchronous-testing/10278) 43 | 2. [Migrate to synchronous style](https://chimp.readme.io/docs/migrate-to-synchronous-style) 44 | 3. [E2E testing your Meteor app with Cucumber, WebdriverIO and Chai] (http://g00glen00b.be/e2e-testing-your-meteor-app-with-cucumber-webdriverio-and-chai/) 45 | 4. [A Basic Cucumber Meteor Tutorial] (http://www.mhurwi.com/a-basic-cucumber-meteor-tutorial/) 46 | 5. [Cucumber.js and Meteor - The why and how of it] (http://joshowens.me/cucumber-js-and-meteor-the-why-and-how-of-it/) 47 | 6. [Writing Effective Cucumber Tests] (https://www.coveros.com/writing-effective-cucumber-tests/) 48 | 7. [15 Expert Tips for Using Cucumber] (https://blog.engineyard.com/2009/15-expert-tips-for-using-cucumber) 49 | 8. [Cucumber.js and Meteor - The why and how of it] (http://joshowens.me/cucumber-js-and-meteor-the-why-and-how-of-it/) 50 | 51 | #####1.2 Videos 52 | 1. [Test a Meteor app with Cucumber.js] (https://www.youtube.com/watch?v=aLlHMToDb6I) 53 | 2. [Cucumber.js and Meteor - The why and how of it] (https://www.youtube.com/watch?v=FiClbcyxTGU) 54 | 55 | ###2. Meteor 56 | 1. [Meteor.js Testing] (http://webtempest.com/meteor-js-testing) 57 | 2. [Test-Driven Meteor: A Very Basic Tutorial] (http://www.mhurwi.com/test-driven-meteor-very-basic-tutorial/) 58 | 3. [Bullet-proof Meteor applications with Velocity, Unit Testing, Integration Testing and Jasmine] (https://doctorllama.wordpress.com/2014/09/22/bullet-proof-internationalised-meteor-applications-with-velocity-unit-testing-integration-testing-and-jasmine/) 59 | 4. [Unit testing Meteor applications with Velocity, Jasmine and Sinon.js] (http://g00glen00b.be/unit-testing-meteor-applications-with-velocity-jasmine-and-sinon-js/) 60 | 61 | ###3. React 62 | 1. [Unit Test React Components in Meteor] (https://medium.com/@skinnygeek1010/unit-test-react-components-in-meteor-a19d96684d7d) 63 | 2. [Unit testing React components without a DOM] (http://simonsmith.io/unit-testing-react-components-without-a-dom/) 64 | 3. [Approaches to testing React components - an overview] (http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/) 65 | 4. [Unit Testing and Building a React Component With Jest, Gulp and React Test Utils](http://www.undefinednull.com/2015/05/03/react-tdd-example-unit-testing-and-building-a-react-component-with-jest-gulp-and-react-test-utils/) 66 | 5. [Unit and Functional Testing React Components] (http://reactjsnews.com/testing-in-react/) 67 | 6. [How React Components Make UI Testing Easy] (http://www.toptal.com/react/how-react-components-make-ui-testing-easy) 68 | 69 | 70 | ##The packages I use are: 71 | 72 | 1. [react] (https://github.com/meteor/react-packages) (It is not officially announced by MDG yet) 73 | 2. [meteorhacks:flow-router] (https://atmospherejs.com/meteorhacks/flow-router) 74 | 3. [twbs:bootstrap](https://atmospherejs.com/twbs/bootstrap) 75 | 4. [accounts-password](https://atmospherejs.com/meteor/accounts-password) 76 | 5. [xolvio:cucumber] (https://atmospherejs.com/xolvio/cucumber) 77 | 6. [sanjo:jasmine](https://atmospherejs.com/sanjo/jasmine) 78 | 79 | 80 | ##Suggestions 81 | Feel free to open a new issue if you find any mistakes or you want further explanations or you want to suggest something else. 82 | I am quite new on React and Testing so I tried to do my best. 83 | 84 | This repo isn't officially supported by [Discover Meteor](http://www.discovermeteor.com). 85 | -------------------------------------------------------------------------------- /client/helpers/error.js: -------------------------------------------------------------------------------- 1 | // Local (client-only) collection 2 | Errors = new Mongo.Collection(null); 3 | 4 | throwError = function (message) { 5 | Errors.insert({message: message}) 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Microscope with React and Flow Router 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /client/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | .grid-block, .main, .post, .comments li, .comment-form { 2 | background: #fff; 3 | border-radius: 3px; 4 | padding: 10px; 5 | margin-bottom: 10px; 6 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 7 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 8 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 9 | } 10 | 11 | body { 12 | background: #eee; 13 | color: #666666; 14 | } 15 | 16 | #main { 17 | position: relative; 18 | } 19 | 20 | .page { 21 | position: absolute; 22 | top: 0px; 23 | width: 100%; 24 | } 25 | 26 | .navbar { 27 | margin-bottom: 10px; 28 | } 29 | 30 | /* line 32, ../sass/style.scss */ 31 | .navbar .navbar-inner { 32 | border-radius: 0px 0px 3px 3px; 33 | } 34 | 35 | #spinner { 36 | height: 300px; 37 | } 38 | 39 | .post { 40 | /* For modern browsers */ 41 | /* For IE 6/7 (trigger hasLayout) */ 42 | *zoom: 1; 43 | position: relative; 44 | opacity: 1; 45 | } 46 | 47 | .post:before, .post:after { 48 | content: ""; 49 | display: table; 50 | } 51 | 52 | .post:after { 53 | clear: both; 54 | } 55 | 56 | .post.invisible { 57 | opacity: 0; 58 | } 59 | 60 | .post.instant { 61 | -webkit-transition: none; 62 | -moz-transition: none; 63 | -o-transition: none; 64 | transition: none; 65 | } 66 | 67 | .post.animate { 68 | -webkit-transition: all 300ms 0ms; 69 | -moz-transition: all 300ms 0ms ease-in; 70 | -o-transition: all 300ms 0ms ease-in; 71 | transition: all 300ms 0ms ease-in; 72 | } 73 | 74 | .post .upvote { 75 | display: block; 76 | margin: 7px 12px 0 0; 77 | float: left; 78 | } 79 | 80 | .post .post-content { 81 | float: left; 82 | } 83 | 84 | .post .post-content h3 { 85 | margin: 0; 86 | line-height: 1.4; 87 | font-size: 18px; 88 | } 89 | 90 | .post .post-content h3 a { 91 | display: inline-block; 92 | margin-right: 5px; 93 | } 94 | 95 | .post .post-content h3 span { 96 | font-weight: normal; 97 | font-size: 14px; 98 | display: inline-block; 99 | color: #aaaaaa; 100 | } 101 | 102 | .post .post-content p { 103 | margin: 0; 104 | } 105 | 106 | .post .discuss { 107 | display: block; 108 | float: right; 109 | margin-top: 7px; 110 | } 111 | 112 | .comments { 113 | list-style-type: none; 114 | margin: 0; 115 | } 116 | 117 | .comments li h4 { 118 | font-size: 16px; 119 | margin: 0; 120 | } 121 | 122 | .comments li h4 .date { 123 | font-size: 12px; 124 | font-weight: normal; 125 | } 126 | 127 | .comments li h4 a { 128 | font-size: 12px; 129 | } 130 | 131 | .comments li p:last-child { 132 | margin-bottom: 0; 133 | } 134 | 135 | .dropdown-menu span { 136 | display: block; 137 | padding: 3px 20px; 138 | clear: both; 139 | line-height: 20px; 140 | color: #bbb; 141 | white-space: nowrap; 142 | } 143 | 144 | .load-more { 145 | display: block; 146 | border-radius: 3px; 147 | background: rgba(0, 0, 0, 0.05); 148 | text-align: center; 149 | height: 60px; 150 | line-height: 60px; 151 | margin-bottom: 10px; 152 | } 153 | 154 | .load-more:hover { 155 | text-decoration: none; 156 | background: rgba(0, 0, 0, 0.1); 157 | } 158 | 159 | .posts .spinner-container { 160 | position: relative; 161 | height: 100px; 162 | } 163 | 164 | .jumbotron { 165 | text-align: center; 166 | } 167 | 168 | .jumbotron h2 { 169 | font-size: 60px; 170 | font-weight: 100; 171 | } 172 | 173 | @-webkit-keyframes fadeOut { 174 | 0% { 175 | opacity: 0; 176 | } 177 | 10% { 178 | opacity: 1; 179 | } 180 | 90% { 181 | opacity: 1; 182 | } 183 | 100% { 184 | opacity: 0; 185 | } 186 | } 187 | 188 | @keyframes fadeOut { 189 | 0% { 190 | opacity: 0; 191 | } 192 | 10% { 193 | opacity: 1; 194 | } 195 | 90% { 196 | opacity: 1; 197 | } 198 | 100% { 199 | opacity: 0; 200 | } 201 | } 202 | 203 | .errors { 204 | position: fixed; 205 | z-index: 10000; 206 | padding: 10px; 207 | top: 0px; 208 | left: 0px; 209 | right: 0px; 210 | bottom: 0px; 211 | pointer-events: none; 212 | } 213 | 214 | .alert { 215 | animation: fadeOut 2700ms ease-in 0s 1 forwards; 216 | -webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards; 217 | -moz-animation: fadeOut 2700ms ease-in 0s 1 forwards; 218 | width: 250px; 219 | float: right; 220 | clear: both; 221 | margin-bottom: 5px; 222 | pointer-events: auto; 223 | } 224 | 225 | .divider{ 226 | width:5px; 227 | height:auto; 228 | display:inline-block; 229 | } 230 | -------------------------------------------------------------------------------- /client/templates/application/not_found.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An Not Found component 3 | */ 4 | NotFound = React.createClass({ 5 | render () { 6 | return ( 7 |
8 |
9 |

404

10 | 11 |

Sorry, we couldn't find a page at this address.

12 |
13 |
14 | ) 15 | } 16 | }); -------------------------------------------------------------------------------- /client/templates/comments/comment_item.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component render a comment 3 | * Props 4 | * author String Comment's author 5 | * submittedText Date Comment's submitted date 6 | * body String Comment's body text 7 | */ 8 | CommentItem = React.createClass({ 9 | //We check the props 10 | propTypes: { 11 | author: React.PropTypes.string.isRequired, 12 | submittedText: React.PropTypes.instanceOf(Date), 13 | body: React.PropTypes.string.isRequired, 14 | }, 15 | render () { 16 | return ( 17 |
18 |
  • 19 |

    20 | {this.props.author} 21 | on {this.props.submittedText.toString()} 22 |

    23 | 24 |

    {this.props.body}

    25 |
  • 26 |
    27 | ) 28 | } 29 | }); -------------------------------------------------------------------------------- /client/templates/comments/comment_submit.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component renders an area in which users can add a comment 3 | * Props 4 | * submitComment Function We call this function from the parent component to add into 5 | * Comments collection the new comment 6 | */ 7 | CommentSubmit = React.createClass({ 8 | propTypes: { 9 | onCommentSubmit: React.PropTypes.func.isRequired 10 | }, 11 | submitComment (event) { 12 | event.preventDefault(); 13 | // We call the parent function to add the new comment 14 | this.props.onCommentSubmit(event.target.body.value); 15 | event.target.body.value = ""; 16 | }, 17 | render () { 18 | return ( 19 |
    20 |
    21 |
    22 |
    23 | 24 | 25 | 26 |
    27 |
    28 | 29 |
    30 |
    31 | ) 32 | } 33 | }); -------------------------------------------------------------------------------- /client/templates/includes/access_denied.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An Access Denied component 3 | */ 4 | AccessDenied = React.createClass({ 5 | render () { 6 | return ( 7 |
    8 |
    9 |

    Access Denied

    10 | 11 |

    You can't get here! Please log in.

    12 |
    13 |
    14 | ) 15 | } 16 | }); -------------------------------------------------------------------------------- /client/templates/includes/errors.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component renders an error 3 | * Props 4 | * message String The message of the error 5 | */ 6 | let Error = React.createClass({ 7 | propsTypes: { 8 | message: React.PropTypes.string.isRequired 9 | }, 10 | render () { 11 | return ( 12 |
    13 |
    14 | 15 | {this.props.message} 16 |
    17 |
    18 | ) 19 | } 20 | }); 21 | 22 | /** 23 | * This component renders a list of errors 24 | * 25 | */ 26 | ErrorsList = React.createClass({ 27 | mixins: [ReactMeteorData], 28 | getMeteorData () { 29 | return { 30 | errors: Errors.find().fetch() 31 | } 32 | }, 33 | render () { 34 | let appErrors = this.data.errors.map(function (error) { 35 | return 39 | }); 40 | return ( 41 |
    42 |
    43 | {appErrors} 44 |
    45 |
    46 | ) 47 | } 48 | }); 49 | 50 | $(document).ready(function () { 51 | React.render(, document.getElementById("errors-section")); 52 | }); -------------------------------------------------------------------------------- /client/templates/includes/header.jsx: -------------------------------------------------------------------------------- 1 | Header = React.createClass({ 2 | mixins: [ReactMeteorData], 3 | componentDidMount() { 4 | // insert Blaze login buttons, see this if you do this a lot 5 | // https://gist.github.com/emdagon/944472f39b58875045b6 6 | var div = document.getElementById('loginContainer'); 7 | Blaze.renderWithData(Template.loginButtons, {align: 'right'}, div); 8 | }, 9 | getMeteorData(props, state) { 10 | // This method knows how to listen to Meteor's reactive data sources, 11 | // Here we change userIsLoggedIn to prevent users to see on the header the Submit Post link 12 | return { 13 | userIsLoggedIn: Meteor.userId() 14 | } 15 | }, 16 | render () { 17 | return ( 18 |
    19 | 41 |
    42 | ); 43 | } 44 | }); 45 | 46 | $(document).ready(function () { 47 | React.render(
    , document.getElementById("header-section")); 48 | }); -------------------------------------------------------------------------------- /client/templates/includes/loading.jsx: -------------------------------------------------------------------------------- 1 | Loading = React.createClass({ 2 | render () { 3 | return ( 4 |
    5 | Loading.. 6 |
    7 | ) 8 | } 9 | }); -------------------------------------------------------------------------------- /client/templates/posts/post_edit.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This componet renders the post edit page 3 | * Renders 4 | * PostInput Component An input component 5 | * Input Html Tag The submit button 6 | * Link Html Tag A button to delete the post 7 | * */ 8 | PostEdit = React.createClass({ 9 | getInitialState () { 10 | return { 11 | errorsTitle: "", 12 | errorsTitleClass: "", 13 | errorsUrl: "", 14 | errorsUrlClass: "" 15 | } 16 | }, 17 | mixins: [ReactMeteorData], 18 | getMeteorData () { 19 | return { 20 | postData: Posts.findOne({_id:this.props._id}) 21 | } 22 | }, 23 | // This functions deletes the post and redirect the user into postsList page 24 | deletePost (event) { 25 | event.preventDefault(); 26 | if (confirm("Delete this post?")) { 27 | Posts.remove(this.props._id); 28 | FlowRouter.go('postsList'); 29 | } 30 | }, 31 | formSubmission (event) { 32 | event.preventDefault(); 33 | // We get the values from inputs 34 | var post = { 35 | url: event.target.url.value, 36 | title: event.target.title.value 37 | }; 38 | // We check if all inputs have values 39 | var fieldsErrors = validatePost(post); 40 | // If we didn't fill any of the inputs then we return a message and an error class 41 | if (!_.isEmpty(fieldsErrors)) { 42 | if (fieldsErrors.title) { 43 | this.setState({ 44 | errorsTitle: fieldsErrors.title, 45 | errorsTitleClass: "has-error" 46 | }); 47 | } else { 48 | this.setState({ 49 | errorsTitle: "", 50 | errorsTitleClass: "" 51 | }); 52 | } 53 | if (fieldsErrors.url) { 54 | this.setState({ 55 | errorsUrl: fieldsErrors.url, 56 | errorsUrlClass: "has-error" 57 | }); 58 | } else { 59 | this.setState({ 60 | errorsUrl: "", 61 | errorsUrlClass: "" 62 | }); 63 | } 64 | return; 65 | } 66 | let postId = this.props._id 67 | Posts.update(postId, {$set: post}, function (error) { 68 | if (error) { 69 | // display the error to the user 70 | throwError(error.reason); 71 | } else { 72 | FlowRouter.go(`/posts/${ postId }`); 73 | } 74 | }) 75 | }, 76 | render () { 77 | if (this.data.postData ) { 78 | return ( 79 |
    this.formSubmission(e)}> 80 | 87 | 94 | 95 |
    96 | Delete post 97 | 98 | ) 99 | } else { 100 | return ( 101 |
    102 | 103 |
    104 | ) 105 | } 106 | 107 | } 108 | }); -------------------------------------------------------------------------------- /client/templates/posts/post_item.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component render a post item. 3 | * Props 4 | * title String Post's Title 5 | * url String Post's Url 6 | * author String Post's author 7 | * commentCount Number Number of comments in the post 8 | */ 9 | PostItem = React.createClass({ 10 | //We check the props 11 | propTypes: { 12 | title: React.PropTypes.string.isRequired, 13 | url: React.PropTypes.string.isRequired, 14 | author: React.PropTypes.string.isRequired, 15 | commentsCount: React.PropTypes.number.isRequired 16 | }, 17 | // This function get a url and return the domain 18 | getDomain (url) { 19 | let a = document.createElement('a'); 20 | a.href = url; 21 | return a.hostname; 22 | }, 23 | // This function called when discussion button clicked and redirect us into post 24 | showPost (event) { 25 | event.preventDefault(); 26 | FlowRouter.go(`/posts/${ this.props._id }`); 27 | }, 28 | render () { 29 | var owner= false; 30 | //We check if the user exists. If exists then we check if he is the owner of the post 31 | if (Meteor.userId()) { 32 | const user = Meteor.user() 33 | owner = this.props.authorId === user._id; 34 | } 35 | let editUrl = `${ this.props._id }/edit`; 36 | return ( 37 |
    38 |
    39 |

    {this.props.title}{this.getDomain(this.props.url)} 40 |

    41 |

    42 | submitted by {this.props.author}, 43 | {this.props.commentsCount} comments 44 | {owner && Edit } 45 |

    46 |
    47 | Discuss 48 |
    49 | ); 50 | } 51 | }); -------------------------------------------------------------------------------- /client/templates/posts/post_list.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component renders a list of posts. Get all post and for each of them pass 3 | * the props in PostItem decodeURIComponent 4 | * 5 | * Render 6 | * PostItem 7 | */ 8 | PostList = React.createClass({ 9 | render () { 10 | // Iterate through all posts and create a post item for each of them 11 | let posts = this.props.allPosts.map(function (post) { 12 | return 20 | }); 21 | return ( 22 |
    23 | {posts} 24 |
    25 | ) 26 | } 27 | }); 28 | 29 | PostListContainer = React.createClass({ 30 | mixins: [ReactMeteorData], 31 | getMeteorData () { 32 | return { 33 | allPosts: Posts.find().fetch() 34 | } 35 | }, 36 | render () { 37 | let renderedComponent = 38 | if (FlowRouter.subsReady("posts")) { 39 | renderedComponent = 40 | } 41 | return ( 42 | renderedComponent 43 | ) 44 | } 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /client/templates/posts/post_page.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component renders the post page. 3 | * Render 4 | * PostItem 5 | * CommentItem A list of comments 6 | * CommentSubmit A component to submit new comment 7 | * Props 8 | * _id String Post's id 9 | */ 10 | PostPage = React.createClass({ 11 | mixins: [ReactMeteorData], 12 | getMeteorData () { 13 | return { 14 | postData: this.getData(), 15 | comments: Comments.find({postId: this.props._id}).fetch(), 16 | userIsLogged: Meteor.userId() 17 | } 18 | }, 19 | getData () { 20 | if (FlowRouter.subsReady()) { 21 | return Posts.findOne(); 22 | } else { 23 | return "Loading..." 24 | } 25 | }, 26 | submitComment (commentText) { 27 | let comment = { 28 | body: commentText, 29 | postId: this.props._id 30 | }; 31 | let errors = {}; 32 | if (!comment.body) { 33 | errors.body = "Please write some content"; 34 | return Session.set('commentSubmitErrors', errors); 35 | } 36 | Meteor.call('commentInsert', comment, function (error, commentId) { 37 | if (error) { 38 | throwError(error.reason); 39 | } 40 | }); 41 | }, 42 | render () { 43 | let post = this.data.postData; 44 | if (FlowRouter.subsReady()) { 45 | let renderedComments = this.data.comments.map(function (comment) { 46 | return 52 | }); 53 | return ( 54 |
    55 | 63 |
      64 | {renderedComments} 65 |
    66 | {this.data.userIsLogged ? 67 | : 69 |

    Please log in to leave a comment.

    70 | } 71 |
    72 | ); 73 | } else { 74 | return ( 75 |
    76 | 77 |
    78 | ) 79 | } 80 | 81 | } 82 | }); -------------------------------------------------------------------------------- /client/templates/posts/post_submit.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component renders a post input form. 3 | * Props 4 | * title String The title of the input field 5 | * placeholder String The placeholder of the input 6 | * errorclassName 7 | * errorMessage 8 | */ 9 | PostInput = React.createClass({ 10 | propTypes: { 11 | title: React.PropTypes.string.isRequired, 12 | placeholder: React.PropTypes.string.isRequired 13 | }, 14 | render () { 15 | const errorClassName = "form-group " + this.props.errorClassName; 16 | const inputClass = "form-control "+this.props.title.toLowerCase(); 17 | return ( 18 |
    19 | 20 | 21 |
    22 | 30 | {this.props.errorMessage} 31 |
    32 |
    33 | ); 34 | } 35 | }); 36 | 37 | /** 38 | * This component renders a form of two input and a button to submit a post 39 | * 40 | */ 41 | PostSubmit = React.createClass({ 42 | mixins: [ReactMeteorData], 43 | getMeteorData () { 44 | return { 45 | userIsLogged: Meteor.userId() 46 | } 47 | }, 48 | // We set 4 different variables 2 for each input 49 | // The one is for the message and the other for the css class 50 | getInitialState () { 51 | return { 52 | errorsTitle: "", 53 | errorsTitleClass: "", 54 | errorsUrl: "", 55 | errorsUrlClass: "", 56 | titleValue: "", 57 | urlValue: "" 58 | } 59 | }, 60 | formSubmission (event) { 61 | event.preventDefault(); 62 | 63 | // We get the values from inputs 64 | var post = { 65 | url: event.target.url.value, 66 | title: event.target.title.value 67 | }; 68 | // We check if all inputs have values 69 | var fieldsErrors = validatePost(post); 70 | // If we didn't fill any of the inputs then we return a message and an error class 71 | if (!_.isEmpty(fieldsErrors)) { 72 | if (fieldsErrors.title) { 73 | this.setState({ 74 | errorsTitle: fieldsErrors.title, 75 | errorsTitleClass: "has-error" 76 | }); 77 | } else { 78 | this.setState({ 79 | errorsTitle: "", 80 | errorsTitleClass: "" 81 | }); 82 | } 83 | if (fieldsErrors.url) { 84 | this.setState({ 85 | errorsUrl: fieldsErrors.url, 86 | errorsUrlClass: "has-error" 87 | }); 88 | } else { 89 | this.setState({ 90 | errorsUrl: "", 91 | errorsUrlClass: "" 92 | }); 93 | } 94 | return; 95 | } 96 | Meteor.call('postInsert', post, function (error, result) { 97 | // display the error to the user and abort 98 | if (error) 99 | return throwError(error.reason); 100 | 101 | // show this result but route anyway 102 | if (result.postExists) 103 | throwError('This link has already been posted'); 104 | 105 | FlowRouter.go(`/posts/${ result._id }`); 106 | }); 107 | }, 108 | render () { 109 | if (this.data.userIsLogged) { 110 | return ( 111 | /** 112 | * We change onSubmit 113 | * from 114 | * onSubmit = {this.formSubmission} 115 | * to 116 | * onSubmit = {(e) => this.formSubmission(e)} 117 | * for unit test events 118 | * More details on this stackoverflow post 119 | * http://stackoverflow.com/questions/26470679/test-a-form-with-jest-and-react-js-testutils 120 | */ 121 |
    this.formSubmission(e)}> 122 | 129 | 136 | 137 | 138 | ); 139 | } else { 140 | return ( 141 | 142 | ); 143 | } 144 | 145 | } 146 | }); -------------------------------------------------------------------------------- /lib/collections/comments.jsx: -------------------------------------------------------------------------------- 1 | Comments = new Mongo.Collection("comments"); 2 | 3 | Meteor.methods({ 4 | commentInsert (commentAttributes) { 5 | check(this.userId, String); 6 | check(commentAttributes, { 7 | postId: String, 8 | body: String 9 | }); 10 | 11 | var user = Meteor.user(); 12 | var post = Posts.findOne(commentAttributes.postId); 13 | 14 | if (!post) 15 | throw new Meteor.Error('invalid-comment', 'You must comment on a post'); 16 | 17 | comment = _.extend(commentAttributes, { 18 | userId: user._id, 19 | author: user.username, 20 | submitted: new Date() 21 | }); 22 | 23 | // update the post with the number of comments 24 | Posts.update(comment.postId, {$inc: {commentsCount: 1}}); 25 | 26 | return Comments.insert(comment); 27 | } 28 | }); -------------------------------------------------------------------------------- /lib/collections/post.jsx: -------------------------------------------------------------------------------- 1 | Posts = new Mongo.Collection("posts"); 2 | 3 | Posts.allow({ 4 | update (userId, post) { return ownsDocument(userId, post); }, 5 | remove (userId, post) { return ownsDocument(userId, post); }, 6 | }); 7 | 8 | validatePost = function (post) { 9 | var errors = {}; 10 | 11 | if (!post.title) 12 | errors.title = "Please fill in a headline"; 13 | 14 | if (!post.url) 15 | errors.url = "Please fill in a URL"; 16 | 17 | return errors; 18 | } 19 | 20 | Meteor.methods({ 21 | postInsert (postAttributes) { 22 | check(this.userId, String); 23 | check(postAttributes, { 24 | title: String, 25 | url: String 26 | }); 27 | 28 | var errors = validatePost(postAttributes); 29 | if (errors.title || errors.url) 30 | throw new Meteor.Error('invalid-post', "You must set a title and URL for your post"); 31 | 32 | var postWithSameLink = Posts.findOne({url: postAttributes.url}); 33 | if (postWithSameLink) { 34 | return { 35 | postExists: true, 36 | _id: postWithSameLink._id 37 | } 38 | } 39 | 40 | var user = Meteor.user(); 41 | var post = _.extend(postAttributes, { 42 | userId: user._id, 43 | author: user.username, 44 | submitted: new Date(), 45 | commentsCount: 0 46 | }); 47 | 48 | var postId = Posts.insert(post); 49 | 50 | return { 51 | _id: postId 52 | }; 53 | } 54 | }); -------------------------------------------------------------------------------- /lib/permissions.js: -------------------------------------------------------------------------------- 1 | // check that the userId specified owns the documents 2 | ownsDocument = (userId, doc) => { 3 | return doc && doc.userId === userId; 4 | } -------------------------------------------------------------------------------- /lib/routes/routers.jsx: -------------------------------------------------------------------------------- 1 | FlowRouter.route('/', { 2 | name: "postsList", 3 | subscriptions (params) { 4 | this.register("posts", Meteor.subscribe("posts")); 5 | }, 6 | action () { 7 | React.render(, document.getElementById("yield-section")); 8 | } 9 | }); 10 | 11 | FlowRouter.route('/posts/:_id', { 12 | name: "postPage", 13 | subscriptions (params) { 14 | this.register("post", Meteor.subscribe("post", params._id)); 15 | this.register("comments", Meteor.subscribe("comments", params._id)); 16 | }, 17 | action (params) { 18 | React.render(, document.getElementById("yield-section")); 19 | } 20 | }); 21 | 22 | FlowRouter.route('/posts/:_id/edit', { 23 | name: "postEdit", 24 | subscriptions (params) { 25 | this.register("post", Meteor.subscribe("post", params._id)); 26 | }, 27 | action (params) { 28 | React.render(, document.getElementById("yield-section")) 29 | } 30 | }); 31 | 32 | FlowRouter.route('/submit', { 33 | name: "postSubmit", 34 | action (params) { 35 | React.render(, document.getElementById("yield-section")); 36 | } 37 | }); 38 | 39 | FlowRouter.route('/authentication', { 40 | name: "authentication", 41 | action (params) { 42 | React.render(, document.getElementById("yield-section")); 43 | } 44 | }); 45 | 46 | FlowRouter.notFound = { 47 | action () { 48 | React.render(, document.getElementById("yield-section")); 49 | } 50 | }; -------------------------------------------------------------------------------- /server/fixtures.js: -------------------------------------------------------------------------------- 1 | // Fixture data 2 | if (Posts.find().count() === 0) { 3 | var now = new Date().getTime(); 4 | 5 | // create two users 6 | var tomId = Meteor.users.insert({ 7 | profile: { name: 'Tom Coleman' } 8 | }); 9 | var tom = Meteor.users.findOne(tomId); 10 | var sachaId = Meteor.users.insert({ 11 | profile: { name: 'Sacha Greif' } 12 | }); 13 | var sacha = Meteor.users.findOne(sachaId); 14 | 15 | var telescopeId = Posts.insert({ 16 | title: 'Introducing Telescope', 17 | userId: sacha._id, 18 | author: sacha.profile.name, 19 | url: 'http://sachagreif.com/introducing-telescope/', 20 | submitted: new Date(now - 7 * 3600 * 1000), 21 | commentsCount: 2 22 | }); 23 | 24 | Comments.insert({ 25 | postId: telescopeId, 26 | userId: tom._id, 27 | author: tom.profile.name, 28 | submitted: new Date(now - 5 * 3600 * 1000), 29 | body: 'Interesting project Sacha, can I get involved?' 30 | }); 31 | 32 | Comments.insert({ 33 | postId: telescopeId, 34 | userId: sacha._id, 35 | author: sacha.profile.name, 36 | submitted: new Date(now - 3 * 3600 * 1000), 37 | body: 'You sure can Tom!' 38 | }); 39 | 40 | Posts.insert({ 41 | title: 'Meteor', 42 | userId: tom._id, 43 | author: tom.profile.name, 44 | url: 'http://meteor.com', 45 | submitted: new Date(now - 10 * 3600 * 1000), 46 | commentsCount: 0 47 | }); 48 | 49 | Posts.insert({ 50 | title: 'The Meteor Book', 51 | userId: tom._id, 52 | author: tom.profile.name, 53 | url: 'http://themeteorbook.com', 54 | submitted: new Date(now - 12 * 3600 * 1000), 55 | commentsCount: 0 56 | }); 57 | } -------------------------------------------------------------------------------- /server/publications.js: -------------------------------------------------------------------------------- 1 | Meteor.publish("posts", function () { 2 | return Posts.find(); 3 | }); 4 | Meteor.publish("post", function (postId) { 5 | return Posts.find({_id:postId}); 6 | }); 7 | Meteor.publish('comments', function(postId) { 8 | check(postId, String); 9 | return Comments.find({postId: postId}); 10 | }); -------------------------------------------------------------------------------- /server/userReg.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | "registerUser": function (userData) { 3 | check(userData, { 4 | username: String, 5 | password: String, 6 | email: String 7 | }); 8 | return Accounts.createUser(userData); 9 | } 10 | }); -------------------------------------------------------------------------------- /tests/cucumber/features/authentication/authentication.feature: -------------------------------------------------------------------------------- 1 | Feature: Allow user to login and logout 2 | 3 | As existing user of the Microscope 4 | I want to login and logout 5 | So that I can prove my identity and see personalized data 6 | 7 | 8 | Background: 9 | Given I am signed out 10 | 11 | @rerun 12 | Scenario: A user can login with valid information 13 | Given I am on the home page 14 | When I click on sign in link 15 | And I enter my authentication information 16 | Then I should be logged in 17 | 18 | @rerun 19 | Scenario: A user cannot login with invalid information 20 | Given I am on the home page 21 | When I click on sign in link 22 | And I enter my false authentication information 23 | Then I should see a user not found error 24 | 25 | @rerun 26 | Scenario: A user cannot login with invalid email address 27 | Given I am on the home page 28 | When I click on sign in link 29 | And I enter my invalid email address 30 | Then I should see an invalid email error message 31 | 32 | @rerun 33 | Scenario: A user cannot login with invalid password 34 | Given I am on the home page 35 | When I click on sign in link 36 | And I enter my invalid password 37 | Then I should see an incorrect password error message 38 | -------------------------------------------------------------------------------- /tests/cucumber/features/authentication/authentication.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | module.exports = function () { 5 | 6 | var actual, expected; 7 | // General variables 8 | var myEmail = "miltos@example.com"; 9 | var myPass = "passpass"; 10 | var signIn = "Sign in"; 11 | var wrongEmail = "stamos@example.com"; 12 | var wrongPass = "wrongPassword"; 13 | var invalidWord = "Invalid"; 14 | 15 | // Npm modules 16 | var url = require("url"); 17 | 18 | /** 19 | * Scenario: A user can login with valid information 20 | */ 21 | 22 | this.Given(/^I am signed out$/, function () { 23 | client.url(process.env.ROOT_URL); 24 | client.waitForExist(".container"); 25 | client.waitForVisible(".container"); 26 | client.waitForVisible("#login-sign-in-link"); 27 | 28 | actual = client.getText("#login-sign-in-link"); 29 | expected = "Sign in"; 30 | 31 | expect(actual).toContain(expected); 32 | }); 33 | 34 | this.Given(/^I am on the home page$/, function () { 35 | // We navigate into home page 36 | client.url(process.env.ROOT_URL); 37 | }); 38 | 39 | this.When(/^I click on sign in link$/, function () { 40 | // We navigate into home page 41 | client.url(process.env.ROOT_URL); 42 | 43 | // Wait for the page to load 44 | client.waitForExist(".container", 1000); 45 | client.waitForVisible(".container", 1000); 46 | // We click the login button 47 | client.click("#login-sign-in-link"); 48 | }); 49 | 50 | this.When(/^I enter my authentication information$/, function () { 51 | return loginWithCredentials(browser, myEmail, myPass) 52 | }); 53 | 54 | this.Then(/^I should be logged in$/, function () { 55 | 56 | //We wait if our email address will appear instead of Sign in 57 | client.waitForExist("#login-name-link"); 58 | 59 | actual = client.getText("#login-name-link"); 60 | expected = myEmail; 61 | expect(actual).toContain(expected); 62 | 63 | }); 64 | 65 | /** 66 | * Scenario: A user cannot login with invalid information 67 | */ 68 | 69 | this.When(/^I enter my false authentication information$/, function () { 70 | return loginWithCredentials(browser, wrongEmail, wrongPass); 71 | }); 72 | 73 | this.Then(/^I should see a user not found error$/, function () { 74 | // We wait the User not found message to appear 75 | client.waitForExist(".error-message"); 76 | 77 | actual = client.getText(".error-message"); 78 | expected = "User not found"; 79 | expect(actual).toContain(expected); 80 | 81 | }); 82 | 83 | /** 84 | * Scenario: A user cannot login with invalid email address 85 | */ 86 | 87 | this.When(/^I enter my invalid email address$/, function () { 88 | return loginWithCredentials(client, invalidWord, wrongPass); 89 | }); 90 | 91 | this.Then(/^I should see an invalid email error message$/, function () { 92 | // We wait the Invalid email message to appear 93 | client.waitForExist(".error-message"); 94 | 95 | actual = client.getText(".error-message"); 96 | expected = "Invalid email"; 97 | expect(actual).toContain(expected); 98 | 99 | }); 100 | 101 | /** 102 | * Scenario: A user cannot login with invalid password 103 | */ 104 | 105 | this.When(/^I enter my invalid password$/, function () { 106 | // We enter into sign in fields wrong information 107 | return loginWithCredentials(client, myEmail, wrongPass); 108 | }); 109 | 110 | this.Then(/^I should see an incorrect password error message$/, function () { 111 | // We wait the Incorrect password message to appear 112 | client.waitForExist(".error-message"); 113 | 114 | actual = client.getText(".error-message"); 115 | expected = "Incorrect password"; 116 | expect(actual).toBe(expected); 117 | }); 118 | } 119 | 120 | /** 121 | * This function get the this object and en email and password and try to login the user 122 | * @param self 123 | * @param email 124 | * @param pass 125 | * @return {*|{phasedRegistrationNames}} 126 | */ 127 | function loginWithCredentials(client, email, pass) { 128 | client.waitForExist("#login-email"); 129 | // We set the values into email and password 130 | client.setValue("#login-email", email); 131 | client.setValue("#login-password", pass); 132 | 133 | // We click the Sign In button 134 | client.click('#login-buttons-password') 135 | } 136 | })(); -------------------------------------------------------------------------------- /tests/cucumber/features/navigation/navigate_steps.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | module.exports = function () { 5 | var actual, expected; 6 | var url = require('url'); 7 | 8 | this.When(/^I navigate to "([^"]*)"$/, function (relativePath) { 9 | //We get from scenario the path and navigate to it 10 | client.url(url.resolve(process.env.ROOT_URL, relativePath)); 11 | client.waitForExist('.app-title'); 12 | }); 13 | 14 | 15 | /** 16 | * Scenario: An unregistered user cannot submit a new post 17 | */ 18 | this.When(/^I navigate to submit page$/, function () { 19 | //We navigate into /submit path 20 | client.url(url.resolve(process.env.ROOT_URL, "/submit")); 21 | client.waitForExist('.app-title'); 22 | }); 23 | 24 | this.Then(/^I should see an Access Denied message$/, function () { 25 | // We wait to see the Access Denied message 26 | client.waitForExist(".access-denied"); 27 | 28 | actual = client.getText(".access-denied h2"); 29 | expected = "Access Denied"; 30 | 31 | expect(actual).toBe(expected); 32 | 33 | }); 34 | 35 | /** 36 | * Scenario: An unregistered user cannot add a comment 37 | */ 38 | this.When(/^I navigate to a post$/, function () { 39 | // We get a postId 40 | var postId =server.call("randomPost"); 41 | // We navigate into post with the appropriate url 42 | client.url(url.resolve(process.env.ROOT_URL, "/posts/" + postId)); 43 | client.waitForExist('.app-title'); 44 | }); 45 | 46 | this.Then(/^I should not be able to insert comment$/, function () { 47 | // We wait for login-leave-comment id and we check if the message 48 | // Please log in to leave a comment. appeared 49 | client.waitForExist("#login-leave-comment"); 50 | 51 | actual = client.getText("#login-leave-comment"); 52 | expected = "Please log in to leave a comment."; 53 | 54 | expect(actual).toBe(expected); 55 | }); 56 | 57 | } 58 | })() 59 | -------------------------------------------------------------------------------- /tests/cucumber/features/navigation/navigation.feature: -------------------------------------------------------------------------------- 1 | Feature: Restrict access to unregistered user 2 | 3 | As unregistered user 4 | I want to navigate only on permitted pages 5 | So that I can see the public content of the page 6 | 7 | Background: 8 | Given I am signed out 9 | 10 | @rerun 11 | Scenario: An unregistered user cannot submit a new post 12 | Given I am on the home page 13 | When I navigate to submit page 14 | Then I should see an Access Denied message 15 | 16 | @rerun 17 | Scenario: An unregistered user cannot add a comment 18 | Given I am on the home page 19 | When I navigate to a post 20 | Then I should not be able to insert comment 21 | -------------------------------------------------------------------------------- /tests/cucumber/features/posts/submit_new_post/submit_new_post.feature: -------------------------------------------------------------------------------- 1 | Feature: Create New Post 2 | 3 | As existing user of the Microscope 4 | I want to login 5 | So that I can submit new post 6 | 7 | Background: Logged in and already at submit page 8 | Given I am logged in 9 | And I navigate to submit new post page 10 | 11 | @rerun 12 | Scenario: Submit a new post 13 | When I fill in all form's fields 14 | And I submit the form 15 | Then I should see the new post 16 | 17 | @rerun 18 | Scenario: Existing Post 19 | When I fill form's fields with existing post 20 | And I submit the form 21 | Then I should see an error message 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/cucumber/features/posts/submit_new_post/submit_new_post.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | module.exports = function () { 5 | var actual, expected; 6 | 7 | /** 8 | * Scenario: Submit a new post 9 | */ 10 | 11 | var url = require('url'); 12 | 13 | this.Given(/^I am logged in$/, function () { 14 | 15 | // Wait 16 | // We navigate into home page 17 | client.url(process.env.ROOT_URL); 18 | 19 | // Wait for the page to load 20 | client.waitForExist(".container", 1000); 21 | client.waitForVisible(".container", 1000); 22 | 23 | // We click the login button 24 | client.click("#login-sign-in-link"); 25 | client.waitForExist("#login-email"); 26 | 27 | // We set the values into email and password 28 | client.setValue("#login-email", "miltos@example.com"); 29 | client.setValue("#login-password", "passpass"); 30 | 31 | // We click the Sign In button 32 | client.click('#login-buttons-password'); 33 | }); 34 | 35 | this.Given(/^I navigate to submit new post page$/, function () { 36 | // We wait for the submit button to exist 37 | client.waitForExist(".submit-post-but"); 38 | client.waitForVisible(".submit-post-but"); 39 | //pause(1000). 40 | // We click the submit button 41 | client.click(".submit-post-but"); 42 | client.isVisible(".sub-post-but"); 43 | }); 44 | 45 | this.When(/^I fill in all form's fields$/, function () { 46 | // We just wait for the submit button to exist and then 47 | // we fill the fields 48 | client.waitForExist(".sub-post-but"); 49 | client.setValue("#title", "Meteor Point"); 50 | client.setValue("#url", "http://www.meteorpoint.com"); 51 | }); 52 | 53 | this.When(/^I submit the form/, function () { 54 | // We click on submit button 55 | client.submitForm(".sub-post-but"); 56 | }); 57 | 58 | this.Then(/^I should see the new post$/, function () { 59 | // After the post submission we wait for post-title to exist and check if it is equal with 60 | // the passing post title from above 61 | client.waitForExist(".post-title"); 62 | 63 | actual = client.getText(".post-title"); 64 | expected = "Meteor Point"; 65 | 66 | expect(actual).toBe(expected); 67 | }); 68 | 69 | /** 70 | * Scenario: Existing Post 71 | */ 72 | 73 | this.Given(/^I fill form's fields with existing post$/, function () { 74 | 75 | client.waitForExist(".sub-post-but"); 76 | client.setValue("#title", "Introducing Telescope"); 77 | client.setValue("#url", "http://sachagreif.com/introducing-telescope/"); 78 | }); 79 | 80 | this.Then(/^I should see an error message$/, function () { 81 | // We check if the error class is visible on the screen 82 | client.waitForExist(".error-alert") 83 | client.isVisible(".error-alert"); 84 | }); 85 | } 86 | 87 | })(); -------------------------------------------------------------------------------- /tests/cucumber/features/static_pages/static_pages.feature: -------------------------------------------------------------------------------- 1 | Feature: Static Pages 2 | 3 | As a new user 4 | I want to be able to access all static pages 5 | So that I can see what this app is all about 6 | 7 | Background: 8 | Given I am a new user 9 | 10 | @rerun 11 | Scenario: Visit home page 12 | When I navigate to "/" 13 | Then I should see the title on the header "Microscope" -------------------------------------------------------------------------------- /tests/cucumber/features/static_pages/static_pages.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | module.exports = function () { 6 | 7 | this.Given(/^I am a new user$/, function (callback) { 8 | callback(); 9 | }); 10 | 11 | this.Then(/^I should see the title on the header "([^"]*)"$/, function (expectedTitle) { 12 | //As a new user when I navigate to home page must see application's title which is Microscope 13 | client.waitForExist('.app-title'); 14 | 15 | var actual = client.getText('.app-title'); 16 | var expected = expectedTitle; 17 | 18 | expect(actual).toBe(expected); 19 | }); 20 | } 21 | })(); -------------------------------------------------------------------------------- /tests/cucumber/features/support/hooks.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | module.exports = function () { 6 | this.Before(function () { 7 | // This code runs before every scenario 8 | server.call("removePosts"); 9 | server.call("addInitialPosts"); 10 | server.call('addUser', {email: "miltos@example.com"}); 11 | }); 12 | }; 13 | 14 | })(); -------------------------------------------------------------------------------- /tests/cucumber/fixtures/posts.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Meteor.methods({ 5 | // We remove all Posts from mirror db 6 | removePosts: function () { 7 | Posts.remove({}); 8 | }, 9 | randomPost: function () { 10 | var postId = Posts.findOne(); 11 | return postId._id; 12 | }, 13 | // We add some initial data 14 | addInitialPosts: function () { 15 | var now = new Date().getTime(); 16 | 17 | // create two users 18 | var tomId = Meteor.users.insert({ 19 | profile: {name: 'Tom Coleman'} 20 | }); 21 | var tom = Meteor.users.findOne(tomId); 22 | var sachaId = Meteor.users.insert({ 23 | profile: {name: 'Sacha Greif'} 24 | }); 25 | var sacha = Meteor.users.findOne(sachaId); 26 | 27 | var telescopeId = Posts.insert({ 28 | title: 'Introducing Telescope', 29 | userId: sacha._id, 30 | author: sacha.profile.name, 31 | url: 'http://sachagreif.com/introducing-telescope/', 32 | submitted: new Date(now - 7 * 3600 * 1000), 33 | commentsCount: 2 34 | }); 35 | 36 | Comments.insert({ 37 | postId: telescopeId, 38 | userId: tom._id, 39 | author: tom.profile.name, 40 | submitted: new Date(now - 5 * 3600 * 1000), 41 | body: 'Interesting project Sacha, can I get involved?' 42 | }); 43 | 44 | Comments.insert({ 45 | postId: telescopeId, 46 | userId: sacha._id, 47 | author: sacha.profile.name, 48 | submitted: new Date(now - 3 * 3600 * 1000), 49 | body: 'You sure can Tom!' 50 | }); 51 | 52 | Posts.insert({ 53 | title: 'Meteor', 54 | userId: tom._id, 55 | author: tom.profile.name, 56 | url: 'http://meteor.com', 57 | submitted: new Date(now - 10 * 3600 * 1000), 58 | commentsCount: 0 59 | }); 60 | 61 | Posts.insert({ 62 | title: 'The Meteor Book', 63 | userId: tom._id, 64 | author: tom.profile.name, 65 | url: 'http://themeteorbook.com', 66 | submitted: new Date(now - 12 * 3600 * 1000), 67 | commentsCount: 0 68 | }) 69 | } 70 | }); 71 | 72 | 73 | })(); -------------------------------------------------------------------------------- /tests/cucumber/fixtures/users.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | 'use strict'; 4 | 5 | Meteor.methods({ 6 | addUser: function (opts) { 7 | Meteor.users.remove({}); 8 | Accounts.createUser({ 9 | email: opts.email, 10 | password: opts.password ? opts.password : "passpass" 11 | }); 12 | } 13 | }); 14 | 15 | })(); -------------------------------------------------------------------------------- /tests/jasmine/client/integration/components/headerComponentSpec.js: -------------------------------------------------------------------------------- 1 | describe("Header Component", function () { 2 | var renderComponentWithProps, post; 3 | 4 | beforeEach(function () { 5 | 6 | renderComponentWithProps = function (component, props, renderType) { 7 | if (renderType === "shallow") { 8 | post = createComponent(component, props); 9 | } else if (renderType == "normal") { 10 | post = renderComponent(component, props); 11 | } 12 | } 13 | }); 14 | 15 | it("should not show post submit button to anonymous user", function () { 16 | // We render our component 17 | renderComponentWithProps(Header, {}, "normal"); 18 | // We try to find the submit button 19 | var postSubmitLink = TestUtils.scryRenderedDOMComponentsWithClass(post, "submit-post-but"); 20 | 21 | // We get the length of the search result 22 | var actual = postSubmitLink.length; 23 | // We expect to be zero 24 | var expected = 0; 25 | expect(actual).toBe(expected); 26 | 27 | }); 28 | 29 | it("should be able to login normal user", function (done) { 30 | // We login our user 31 | Meteor.loginWithPassword("miltos@example.com", "passpass", function (err) { 32 | // We expect not to have errors 33 | expect(err).toBeUndefined(); 34 | done(); 35 | }); 36 | }); 37 | 38 | it("should be show submit post button to registered user", function () { 39 | // We render our component 40 | renderComponentWithProps(Header, {}, "normal"); 41 | // We search for the submit button 42 | var postSubmitLink = TestUtils.scryRenderedDOMComponentsWithClass(post, "submit-post-but"); 43 | 44 | // We get the length of the above search results 45 | var actual = postSubmitLink.length; 46 | // We expect to find our button 47 | var expected = 1; 48 | expect(actual).toBe(expected); 49 | }); 50 | 51 | it("should be able to logout", function (done) { 52 | // We logout our user 53 | Meteor.logout(function (err) { 54 | expect(err).toBeUndefined(); 55 | done(); 56 | }); 57 | }); 58 | 59 | it("should be throw error if email is wrong", function (done) { 60 | // We login our user with wrong email 61 | Meteor.loginWithPassword("WrongUser", "passpass", function (err) { 62 | // We expect errors to be returned 63 | expect(err).toBeDefined(); 64 | done(); 65 | }) 66 | }); 67 | 68 | it("should be throw error if credentials are wrong", function (done) { 69 | // We login our user with wrong password 70 | Meteor.loginWithPassword("miltos@example.com", "WrongPass", function (err) { 71 | // We expect errors to be returned 72 | expect(err).toBeDefined(); 73 | done(); 74 | }) 75 | }) 76 | }); -------------------------------------------------------------------------------- /tests/jasmine/client/integration/fixtures/users.js: -------------------------------------------------------------------------------- 1 | // We add a new user 2 | Meteor.startup(function() { 3 | if (Meteor.users.find().count() == 0) { 4 | var users = [ 5 | {name:"Miltos",email:"miltos@example.com",roles:[], password: "passpass"}, 6 | ]; 7 | _.each(users, function (user) { 8 | var id = Accounts.createUser({ 9 | email: user.email, 10 | password: user.password, 11 | profile: { name: user.name } 12 | }); 13 | }); 14 | }; 15 | }); -------------------------------------------------------------------------------- /tests/jasmine/client/integration/spec_helper.js: -------------------------------------------------------------------------------- 1 | TestUtils = React.addons.TestUtils; 2 | Simulate = TestUtils.Simulate; 3 | 4 | renderComponent = function (comp, props) { 5 | return TestUtils.renderIntoDocument( 6 | React.createElement(comp, props) 7 | ); 8 | }; 9 | 10 | simulateClickOn = function($el) { 11 | React.addons.TestUtils.Simulate.click($el[0]); 12 | }; -------------------------------------------------------------------------------- /tests/jasmine/client/integration/spec_helper_shallow.js: -------------------------------------------------------------------------------- 1 | TestUtils = React.addons.TestUtils; 2 | Simulate = TestUtils.Simulate; 3 | 4 | createComponent = function (component, props){ // ...children) { 5 | const shallowRenderer = TestUtils.createRenderer(); 6 | // We don't render our component into DOM but we make a shallow render which is more fast 7 | //Instead of rendering into a DOM the idea of shallow rendering is to instantiate a component 8 | // and get the result of its render method, which is a ReactElement. 9 | // From here you can do things like check its props and children and verify it works as expected. 10 | // based on this article http://simonsmith.io/unit-testing-react-components-without-a-dom/ 11 | shallowRenderer.render(React.createElement(component, props));//, children.length > 1 ? children : children[0])); 12 | return shallowRenderer.getRenderOutput(); 13 | } 14 | 15 | simulateClickOn = function($el) { 16 | React.addons.TestUtils.Simulate.click($el[0]); 17 | }; -------------------------------------------------------------------------------- /tests/jasmine/client/unit/components/post_item_spec.js: -------------------------------------------------------------------------------- 1 | describe("PostItem", function () { 2 | var defProps, renderWithProps, component, el, $el; 3 | 4 | beforeEach(function() { 5 | defProps = { 6 | _id: "XYZ", 7 | title: "Meteor Point", 8 | url: "http://www.meteorpoint.com", 9 | author: "Miltos", 10 | commentsCount: 5 11 | } 12 | renderWithProps = function(props) { 13 | component = renderComponent(PostItem, props); 14 | el = React.findDOMNode(component); 15 | $el = $(el); 16 | }; 17 | }); 18 | 19 | it("should get the domain from the url", function () { 20 | expect(PostItem.prototype.getDomain("http://www.meteor.com")).toBe("www.meteor.com") 21 | }); 22 | 23 | it("should print out post's title", function () { 24 | // We render the compoent 25 | renderWithProps(defProps); 26 | // We expect post's title to render 27 | // We use the jQuery to get the text from post-title class 28 | expect($el.children().find(".post-title").text()).toEqual("Meteor Point"); 29 | }); 30 | 31 | it("should display Edit button when user is the author", function () { 32 | // We create fake object to pass for Meteor.user() function 33 | var user = { 34 | username: "Miltos" 35 | } 36 | spyOn(Meteor, "user").and.returnValue(user); 37 | spyOn(Meteor, "userId").and.returnValue(true); 38 | // We render the object and we wait at jQuery object $el on text function 39 | // to find the Edit word 40 | renderWithProps(defProps); 41 | expect($el.text()).toContain("Edit"); 42 | 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/jasmine/client/unit/components/post_list_spec.js: -------------------------------------------------------------------------------- 1 | describe("PostList", function () { 2 | var renderComponentWithProps, post, el, $el, posts; 3 | 4 | beforeEach(function () { 5 | 6 | renderComponentWithProps = function (component, props, renderType, data) { 7 | if (renderType === "shallow") { 8 | post = createComponent(component, props); 9 | } else if (renderType == "normal") { 10 | post = renderComponent(component, props); 11 | el = React.findDOMNode(post); 12 | $el = $(el); 13 | } 14 | } 15 | // We set mock data 16 | posts = { 17 | allPosts: [ 18 | {id: 1, title: "First Post", url:"Not found", author:"Miltos", commentsCount: 5}, 19 | {id: 2, title: "Second Post", url:"401 for the win", author:"Milkos", commentsCount: 3} 20 | ] 21 | } 22 | }); 23 | 24 | it("should render a list of posts", function () { 25 | 26 | 27 | // We shallow render our component with our mock data 28 | renderComponentWithProps(PostList, posts , "shallow"); 29 | // We get the number of its children 30 | var actual = post.props.children.length; 31 | // We expect to has 2 posts as the length of mock data 32 | var expected = posts.allPosts.length; 33 | 34 | expect(actual).toBe(expected); 35 | }); 36 | 37 | it("should render a list of PostItem", function () { 38 | 39 | // We shallow render our component with our mock data 40 | renderComponentWithProps(PostList, posts , "shallow"); 41 | // We check for every children of PostList component to be element of type PostItem 42 | var items = post.props.children.filter(function (postLitItem) { 43 | return TestUtils.isElementOfType(postLitItem, PostItem); 44 | }); 45 | // The actual length of items array. 46 | var actual = items.length; 47 | // The expected size of items array. As the number of posts 48 | var expected = posts.allPosts.length; 49 | expect(actual).toBe(expected); 50 | 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/jasmine/client/unit/components/post_submit_spec.js: -------------------------------------------------------------------------------- 1 | describe("PostSubmit", function () { 2 | var renderComponentWithProps, post; 3 | 4 | beforeEach(function () { 5 | 6 | renderComponentWithProps = function (component, props, renderType) { 7 | if (renderType === "shallow") { 8 | post = createComponent(component, props); 9 | } else if (renderType == "normal") { 10 | post = renderComponent(component, props); 11 | } 12 | } 13 | }); 14 | 15 | describe("User is logged in", function () { 16 | 17 | beforeEach(function () { 18 | // We spyOn Meteor.userId to provide a user id 19 | spyOn(Meteor, "userId").and.returnValue("xyz"); 20 | }); 21 | 22 | it("should generate a submit form", function () { 23 | // We shallow render our component 24 | renderComponentWithProps(PostSubmit, {}, "shallow"); 25 | // We get the length of component's children 26 | var actual = post.props.children.length; 27 | // We expect to has 3 children the 2 input fields and the button 28 | var expected = 3; 29 | expect(actual).toBe(expected); 30 | }); 31 | 32 | it("should render an input for post's title", function () { 33 | // We render the component 34 | renderComponentWithProps(PostSubmit, {}, "shallow"); 35 | //We get the post title children from PostSubmit component 36 | var postTitle = post.props.children[0]; 37 | 38 | // We write what is the actual output and what the expected to 39 | // make our tests more readable 40 | var actual = postTitle.props.title; 41 | var expected = "Title"; 42 | 43 | expect(actual).toBe(expected); 44 | }); 45 | 46 | it("should render an input for post's url", function () { 47 | // We render the component 48 | renderComponentWithProps(PostSubmit, {}, "shallow"); 49 | // We get the post url children from PostSubmit component 50 | var postUrl = post.props.children[1]; 51 | 52 | var actual = postUrl.props.title; 53 | var expected = "URL"; 54 | 55 | expect(actual).toBe(expected); 56 | }); 57 | 58 | it("should render an error when the title is empty", function () { 59 | // We render our component 60 | renderComponentWithProps(PostSubmit, {}, "normal"); 61 | // We get all input fields from our component 62 | var inputs = TestUtils.scryRenderedDOMComponentsWithTag(post, "input"); 63 | // We find the title input component 64 | var titleInput = inputs.find((el) => { 65 | return el.props.name == 'title' 66 | }); 67 | // We find the error area above title input. These area has a span tag 68 | var titleError = React.findDOMNode(titleInput).parentNode.querySelector("span"); 69 | 70 | 71 | // We search for form tag into rendered component 72 | var form = TestUtils.findRenderedDOMComponentWithTag(post, "form"); 73 | // We simulate the submission 74 | // on this submission the default value for title and url are "" (empty) 75 | // so after submit we will have an error message 76 | TestUtils.Simulate.submit(form.getDOMNode()); 77 | 78 | expect(titleError.innerHTML).toBe("Please fill in a headline"); 79 | }); 80 | 81 | it("should render an error when the url input is empty", function () { 82 | // We render our component 83 | renderComponentWithProps(PostSubmit, {}, "normal"); 84 | // We get all input fields from our component 85 | var inputs = TestUtils.scryRenderedDOMComponentsWithTag(post, "input"); 86 | // We find the url input component 87 | var urlInput = inputs.find((el) => { 88 | return el.props.name == 'url' 89 | }); 90 | // We find the error section below url input 91 | var urlError = React.findDOMNode(urlInput).parentNode.querySelector("span"); 92 | // We find the title input component 93 | var titleInput = inputs.find((el) => { 94 | return el.props.name == 'title' 95 | }); 96 | // We change the value of the title input so only the url input to be empty 97 | TestUtils.Simulate.change(titleInput, {target: {value: "someValue"}}); 98 | // We search for form tag into rendered component 99 | var form = TestUtils.findRenderedDOMComponentWithTag(post, "form"); 100 | // We simulate the submission 101 | // on this submission the default value for url is "" (empty) 102 | // so after submit we will have an error message 103 | TestUtils.Simulate.submit(form.getDOMNode()); 104 | // We expect to be rendered the error message for url 105 | expect(urlError.innerHTML).toBe("Please fill in a URL"); 106 | }); 107 | 108 | 109 | it("should call formSubmission when submit the form", () => { 110 | // We render into DOM our component 111 | renderComponentWithProps(PostSubmit, {}, "normal"); 112 | // We spy on formSubmission function which is responsible 113 | // to submit the new post 114 | spyOn(post, "formSubmission"); 115 | 116 | // We search for form tag into rendered component 117 | var form = TestUtils.findRenderedDOMComponentWithTag(post, "form"); 118 | // We simulate the submission 119 | TestUtils.Simulate.submit(form.getDOMNode()); 120 | // We expect after the submission our function to have been called 121 | expect(post.formSubmission).toHaveBeenCalled(); 122 | }); 123 | }); 124 | 125 | describe("User is not logged in", function () { 126 | beforeEach(function () { 127 | // We spyOn Meteor.userId to provide null userId 128 | spyOn(Meteor, "userId").and.returnValue(null); 129 | }); 130 | 131 | it("should render AccessDenied component ", () => { 132 | // We render the component 133 | renderComponentWithProps(PostSubmit, {}, "shallow"); 134 | 135 | 136 | var actual = post.type; 137 | var expected = AccessDenied; 138 | 139 | expect(actual).toBe(expected); 140 | }); 141 | }); 142 | 143 | 144 | }); -------------------------------------------------------------------------------- /tests/jasmine/client/unit/spec_helper.js: -------------------------------------------------------------------------------- 1 | TestUtils = React.addons.TestUtils; 2 | Simulate = TestUtils.Simulate; 3 | 4 | renderComponent = function (comp, props) { 5 | return TestUtils.renderIntoDocument( 6 | React.createElement(comp, props) 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /tests/jasmine/client/unit/spec_helper_shallow.js: -------------------------------------------------------------------------------- 1 | TestUtils = React.addons.TestUtils; 2 | Simulate = TestUtils.Simulate; 3 | 4 | createComponent = function (component, props){ // ...children) { 5 | const shallowRenderer = TestUtils.createRenderer(); 6 | // We don't render our component into DOM but we make a shallow render which is more fast 7 | //Instead of rendering into a DOM the idea of shallow rendering is to instantiate a component 8 | // and get the result of its render method, which is a ReactElement. 9 | // From here you can do things like check its props and children and verify it works as expected. 10 | // based on this article http://simonsmith.io/unit-testing-react-components-without-a-dom/ 11 | shallowRenderer.render(React.createElement(component, props));//, children.length > 1 ? children : children[0])); 12 | return shallowRenderer.getRenderOutput(); 13 | } 14 | 15 | simulateClickOn = function($el) { 16 | React.addons.TestUtils.Simulate.click($el[0]); 17 | }; --------------------------------------------------------------------------------