From aef05db5e50f6f38b47d0b61131e501548208cea Mon Sep 17 00:00:00 2001 From: Jonathan Wohl Date: Thu, 12 Nov 2015 15:31:09 -0500 Subject: [PATCH 01/69] added babel-jest for js tests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index de17f0d6..e4467fee 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "screenfull": "^2.0.0" }, "devDependencies": { + "babel-jest": "^6.0.1", "babelify": "^6.1.2", "browserify": "^10.2.3", "browserify-shim": "^3.8.7", From 61c359c48d872860f48c5f298b5e62b293922294 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl Date: Fri, 13 Nov 2015 10:37:31 -0500 Subject: [PATCH 02/69] WIP - updated eslint to include jest stuff --- .eslintrc | 5 ++++- package.json | 16 +++++++++++++--- tests/js/example-test.js | 18 ++++++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.eslintrc b/.eslintrc index 7ed608d3..db5ed36c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -25,11 +25,14 @@ "browser": true, "node": true, "commonjs": true, - "jquery": true + "jquery": true, + "jest": true }, "extends": "eslint:recommended", "ecmaFeatures": { "jsx": true, + "modules": true, + "arrowFunctions": true, "experimentalObjectRestSpread": true }, "plugins": [ diff --git a/package.json b/package.json index e4467fee..f5a8668a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "pouchdb": "^4.0.3", "pouchdb-upsert": "^1.1.1", "ratchet": "https://github.com/twbs/ratchet/archive/v2.0.2.tar.gz", - "react": "^0.13.3", + "react": "^0.14.2", + "react-dom": "~0.14.2", "screenfull": "^2.0.0" }, "devDependencies": { @@ -48,7 +49,7 @@ "gulp-streamify": "^1.0.2", "gulp-uglify": "1.1.0", "istanbul": "^0.3.13", - "jest-cli": "^0.5.0", + "jest-cli": "^0.5.10", "jsdom": "^5.1.0", "jshint": "^2.7.0", "mocha": "^2.2.4", @@ -79,6 +80,15 @@ "screenfull": "global:screenfull" }, "jest": { + "scriptPreprocessor": "/node_modules/babel-jest", + "testFileExtensions": ["es6", "js"], + "moduleFileExtensions": ["js", "json", "es6"], + "unmockedModulePathPatterns": [ + "/node_modules/react", + "/node_modules/react-dom", + "/node_modules/react-addons-test-utils", + "/node_modules/fbjs" + ], "testDirectoryName": "tests/js", "collectCoverage": true, "collectCoverageOnlyFrom": { @@ -127,7 +137,7 @@ "Data", "Collection" ], - "author": "Abdi Dahir Viktor Roytman Jon Wohl", + "author": "Abdi Dahir, Viktor Roytman, & Jonathan Wohl", "license": "ISC", "bugs": { "url": "https://github.com/SEL-Columbia/dokomoforms/issues" diff --git a/tests/js/example-test.js b/tests/js/example-test.js index 46234faf..35dc3e31 100644 --- a/tests/js/example-test.js +++ b/tests/js/example-test.js @@ -1,6 +1,16 @@ //TODO: deleteme! -describe('a', function() { - it('is', function() { - expect(3).toBe(4); - }); +// jest.dontMock('../../dokomoforms/static/src/survey/js/components/Header'); + +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// const Header = require('../../dokomoforms/static/src/survey/js/components/Header'); + +describe('Header', () => { + + it('does something', () => { + expect(3).toEqual(3); + }); + }); From ac79848e48d7c0f8d61492ce036b342a13d94186 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl Date: Mon, 16 Nov 2015 17:14:53 -0500 Subject: [PATCH 03/69] added some initial js tests, setup jest for ES6 tests --- .babelrc | 3 + .eslintrc | 1 + .../js/components/baseComponents/BigButton.js | 12 +- .../js/components/baseComponents/Card.js | 2 +- .../js/components/baseComponents/DontKnow.js | 22 +- .../components/baseComponents/LittleButton.js | 19 +- .../js/components/baseComponents/Menu.js | 3 - .../components/baseComponents/PhotoField.js | 15 +- .../components/baseComponents/PhotoPreview.js | 34 +- .../baseComponents/ResponseField.js | 21 +- .../js/components/baseComponents/Title.js | 12 +- package.json | 66 +-- tests/js/base-component-tests.js | 380 ++++++++++++++++++ tests/js/example-test.js | 16 - 14 files changed, 474 insertions(+), 132 deletions(-) create mode 100644 .babelrc create mode 100644 tests/js/base-component-tests.js delete mode 100644 tests/js/example-test.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..d4e5f8d4 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react"] +} diff --git a/.eslintrc b/.eslintrc index db5ed36c..379472c5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -33,6 +33,7 @@ "jsx": true, "modules": true, "arrowFunctions": true, + "blockBindings": true, "experimentalObjectRestSpread": true }, "plugins": [ diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js b/dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js index 6ca9a2d5..33152afa 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js @@ -18,12 +18,12 @@ module.exports = React.createClass({ } return ( -
- -
- ); +
+ +
+ ); } }); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/Card.js b/dokomoforms/static/src/survey/js/components/baseComponents/Card.js index e44e108e..93d8a5a6 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/Card.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/Card.js @@ -21,7 +21,7 @@ module.exports = React.createClass({
{this.props.messages.map(function(msg, idx) { return ( - {msg}
+ {msg}
); })}
diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js b/dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js index 99f75425..c259be89 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js @@ -9,17 +9,17 @@ var React = require('react'); module.exports = React.createClass({ render: function() { return ( -
- - -
- ); +
+ + +
+ ); } }); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js b/dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js index f4a83078..dd9885fd 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js @@ -8,6 +8,7 @@ var React = require('react'); * @buttonFunction: What to do on click events * @text: Text of the button * @icon: Icon if any to show before button text + * @disabled: Whether or not the button should be disabled */ module.exports = React.createClass({ render: function() { @@ -15,16 +16,16 @@ module.exports = React.createClass({ var classes = 'btn '; classes += this.props.extraClasses || ''; return ( -
- -
- ); + {this.props.icon ? : null } + {this.props.text} + + + ); } }); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/Menu.js b/dokomoforms/static/src/survey/js/components/baseComponents/Menu.js index 2a68f068..f1b088ad 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/Menu.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/Menu.js @@ -138,9 +138,6 @@ module.exports = React.createClass({ ); } - console.log('loggedIn: ', this.props.loggedIn); - console.log('hasFacilities: ', this.props.hasFacilities); - if (navigator.onLine) { if (this.props.hasFacilities) { reloadFacilities = ( diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js b/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js index c365181c..827a30cc 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js @@ -2,8 +2,9 @@ var React = require('react'), PhotoPreview = require('./PhotoPreview'); /* - * ResponseField component - * Main input field component, handles validation + * PhotoField component + * + * Displays a photo thumbnail. * * props: * @onInput: What to do on valid input @@ -49,16 +50,6 @@ module.exports = React.createClass({ this.props.buttonFunction(this.props.index); }, - /* - * Handle change event, validates on every change - * fires props.onInput on validation success - * - * @event: Change event - */ - // onChange: function(event) { - - // }, - render: function() { var preview; if (this.state.showPreview) { diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js b/dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js index 294a1922..62075960 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js @@ -1,39 +1,17 @@ var React = require('react'); /* - * ResponseField component - * Main input field component, handles validation + * PhotoPreview component + * + * Displays a preview of the Photo * * props: - * @onInput: What to do on valid input - * @index: What index value to send on valid input (i.e position in array of fields) - * @showRetake: Show the 'retake' button - * @buttonFunction: What to do on 'X' click event, index value is bound to this function - * @initValue: Initial value for the input field + * @onClose: What to do when the close button is pressed + * @onDelete: What to do when the delete button is pressed + * @url: The URL of the photo to display */ module.exports = React.createClass({ - /* - * Validate the answer based on props.type - * - * @answer: The response to be validated - * - * TODO: implement photo validation, if necessary... - */ - validate: function(answer) { - return true; - }, - - /* - * Handle change event, validates on every change - * fires props.onInput on validation success - * - * @event: Change event - */ - // onChange: function(event) { - - // }, - render: function() { var divStyle = { backgroundImage: 'url(' + this.props.url + ')' diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js index a38e09de..7e1fc530 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js @@ -16,14 +16,13 @@ var React = require('react'); */ module.exports = React.createClass({ getInitialState: function() { - return { - }; + return {}; }, // Determine the input field type based on props.type getResponseType: function() { var type = this.props.type; - switch(type) { + switch (type) { case 'integer': case 'decimal': return 'number'; @@ -43,7 +42,7 @@ module.exports = React.createClass({ // Determine the input field step based on props.type getResponseStep: function() { var type = this.props.type; - switch(type) { + switch (type) { case 'decimal': return 'any'; case 'timestamp': @@ -63,7 +62,7 @@ module.exports = React.createClass({ var logic = this.props.logic; console.log('Enforcing: ', logic); var val = null; - switch(type) { + switch (type) { case 'integer': val = parseInt(answer); if (isNaN(val)) { @@ -109,7 +108,7 @@ module.exports = React.createClass({ var month = ('0' + (resp.getMonth() + 1)).slice(-2); var year = resp.getFullYear(); val = answer; //XXX Keep format? - if(isNaN(year) || isNaN(month) || isNaN(day)) { + if (isNaN(year) || isNaN(month) || isNaN(day)) { val = null; } @@ -130,9 +129,9 @@ module.exports = React.createClass({ case 'time': //TODO: enforce default: - if (answer) { - val = answer; - } + if (answer) { + val = answer; + } } return val; @@ -161,7 +160,7 @@ module.exports = React.createClass({ render: function() { return ( -
+
- ); + ); } }); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/Title.js b/dokomoforms/static/src/survey/js/components/baseComponents/Title.js index daa80894..a3a0af6d 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/Title.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/Title.js @@ -9,11 +9,11 @@ var React = require('react'); */ module.exports = React.createClass({ render: function() { - return ( -
-

{this.props.title}

-

{this.props.message}

-
- ) + return ( +
+

{this.props.title}

+

{this.props.message}

+
+ ); } }); diff --git a/package.json b/package.json index 98e72fd4..4bee729a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,10 @@ "screenfull": "^2.0.0" }, "devDependencies": { + "babel-core": "^6.1.20", "babel-jest": "^6.0.1", + "babel-preset-es2015": "^6.1.18", + "babel-preset-react": "^6.1.18", "babelify": "^7.2.0", "browserify": "^12.0.1", "browserify-shim": "^3.8.7", @@ -55,6 +58,7 @@ "mocha": "^2.2.4", "mocha-istanbul": "^0.2.0", "node-underscorify": "0.0.14", + "react-addons-test-utils": "~0.14.2", "reactify": "^1.1.1", "should": "^7.1.1", "supertest": "^1.1.0", @@ -81,8 +85,12 @@ }, "jest": { "scriptPreprocessor": "/node_modules/babel-jest", - "testFileExtensions": ["es6", "js"], - "moduleFileExtensions": ["js", "json", "es6"], + "testFileExtensions": [ + "js" + ], + "moduleFileExtensions": [ + "js" + ], "unmockedModulePathPatterns": [ "/node_modules/react", "/node_modules/react-dom", @@ -92,33 +100,33 @@ "testDirectoryName": "tests/js", "collectCoverage": true, "collectCoverageOnlyFrom": { - "dokomoforms/static/Application.js": true, - "dokomoforms/static/FacilityAPI.js": true, - "dokomoforms/static/PhotoAPI.js": true, - "dokomoforms/static/persona.js": true, - "dokomoforms/static/visualizations.js": true, - "dokomoforms/static/components/Facility.js": true, - "dokomoforms/static/components/Footer.js": true, - "dokomoforms/static/components/Header.js": true, - "dokomoforms/static/components/Location.js": true, - "dokomoforms/static/components/MultipleChoice.js": true, - "dokomoforms/static/components/Note.js": true, - "dokomoforms/static/components/Photo.js": true, - "dokomoforms/static/components/Question.js": true, - "dokomoforms/static/components/Splash.js": true, - "dokomoforms/static/components/Submit.js": true, - "dokomoforms/static/components/baseComponents/BigButton.js": true, - "dokomoforms/static/components/baseComponents/Card.js": true, - "dokomoforms/static/components/baseComponents/DontKnow.js": true, - "dokomoforms/static/components/baseComponents/FacilityRadios.js": true, - "dokomoforms/static/components/baseComponents/LittleButton.js": true, - "dokomoforms/static/components/baseComponents/Menu.js": true, - "dokomoforms/static/components/baseComponents/Message.js": true, - "dokomoforms/static/components/baseComponents/PhotoField.js": true, - "dokomoforms/static/components/baseComponents/ResponseField.js": true, - "dokomoforms/static/components/baseComponents/ResponseFields.js": true, - "dokomoforms/static/components/baseComponents/Select.js": true, - "dokomoforms/static/components/baseComponents/Title.js": true + "dokomoforms/static/src/survey/js/Application.js": false, + "dokomoforms/static/src/survey/js/api/FacilityAPI.js": false, + "dokomoforms/static/src/survey/js/api/PhotoAPI.js": false, + "dokomoforms/static/src/common/js/persona.js": false, + "dokomoforms/static/src/survey/js/components/Facility.js": false, + "dokomoforms/static/src/survey/js/components/Footer.js": false, + "dokomoforms/static/src/survey/js/components/Header.js": false, + "dokomoforms/static/src/survey/js/components/Location.js": false, + "dokomoforms/static/src/survey/js/components/MultipleChoice.js": false, + "dokomoforms/static/src/survey/js/components/Note.js": false, + "dokomoforms/static/src/survey/js/components/Photo.js": false, + "dokomoforms/static/src/survey/js/components/Question.js": false, + "dokomoforms/static/src/survey/js/components/Splash.js": false, + "dokomoforms/static/src/survey/js/components/Submit.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/Card.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/Message.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/Menu.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/Select.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/Title.js": false } }, "scripts": { diff --git a/tests/js/base-component-tests.js b/tests/js/base-component-tests.js new file mode 100644 index 00000000..6b65926c --- /dev/null +++ b/tests/js/base-component-tests.js @@ -0,0 +1,380 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Card.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Message.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Title.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); +jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js'); +// jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Menu.js'); + +const BigButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton'); +const LittleButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton'); +const Card = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Card'); +const Message = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Message'); +const Title = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Title'); +const DontKnow = require('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow'); +const PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); +const PhotoField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField'); +// const Menu = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Menu'); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('BigButton', () => { + + it('renders displaying its text property', () => { + // Render a BigButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // Get the rendered element + var buttonNode = ReactDOM.findDOMNode(button); + + // Verify that its text matches Press Me! + expect(buttonNode.textContent).toEqual('Press Me!'); + }); + + it('calls the buttonFunction property when pressed', () => { + var callback = jest.genMockFunction(); + + // Render a BigButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(button, 'button') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('should add a class associated with a type property', () => { + // Render a BigButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // makes sure the type property has added the class + TestUtils.findRenderedDOMComponentWithClass(button, 'btn-testing'); + }); + +}); + +describe('LittleButton', () => { + + it('renders displaying its text property', () => { + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // Get the rendered element + var buttonNode = ReactDOM.findDOMNode(button); + + // Verify that its text matches Press Me! + expect(buttonNode.textContent).toEqual('Press Me!'); + }); + + it('calls the buttonFunction property when pressed', () => { + var callback = jest.genMockFunction(); + + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(button, 'button') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('is disabled when disabled property is set', () => { + var callback = jest.genMockFunction(); + + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(button, 'button') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(0); + }); + + it('includes an icon if an icon property is set', () => { + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + + ); + + // makes sure there is a span tag (i.e. icon) rendered within the button + TestUtils.findRenderedDOMComponentWithTag(button, 'span'); + }); + +}); + +describe('Card', () => { + + it('should render a sinlge message', () => { + var message = ['hello']; + + var card = TestUtils.renderIntoDocument( + + ); + + // makes sure there is a single span tag (i.e. message) + TestUtils.findRenderedDOMComponentWithTag(card, 'span'); + }); + + it('should render multiple messages', () => { + var messages = ['hello', 'hi', 'hola']; + + var card = TestUtils.renderIntoDocument( + + ); + + // makes sure there is a single span tag (i.e. message) + var spans = TestUtils.scryRenderedDOMComponentsWithTag(card, 'span'); + + expect(spans.length).toEqual(3); + }); + + it('should add a class associated with a type property', () => { + var message = ['hello']; + + var card = TestUtils.renderIntoDocument( + + ); + + // makes sure there the type prop has added the class + TestUtils.findRenderedDOMComponentWithClass(card, 'message-secondary'); + }); +}); + +describe('Message', () => { + + it('should render a sinlge message', () => { + var message = TestUtils.renderIntoDocument( + + ); + + // Get the rendered element + var messageNode = ReactDOM.findDOMNode(message); + + // Verify that its text matches Press Me! + expect(messageNode.textContent).toEqual('Hello'); + }); + + + it('should add classes associated with the classes property', () => { + var classes = 'testing'; + + var message = TestUtils.renderIntoDocument( + + ); + + // makes sure there the type prop has added the class + TestUtils.findRenderedDOMComponentWithClass(message, 'testing'); + }); +}); + +describe('Title', () => { + + it('should render a title with a message', () => { + var title = TestUtils.renderIntoDocument( + + ); + + var titleNode = TestUtils.findRenderedDOMComponentWithTag(title, 'h3'); + var messageNode = TestUtils.findRenderedDOMComponentWithTag(title, 'p'); + + // Verify that its text matches Press Me! + expect(titleNode.textContent).toEqual('Hello'); + expect(messageNode.textContent).toEqual('Hola'); + }); +}); + + +describe('DontKnow', () => { + + it('calls the checkBoxFunction property when pressed', () => { + var callback = jest.genMockFunction(); + + // Render a DontKnow in the document + var dontKnow = TestUtils.renderIntoDocument( + <DontKnow checkBoxFunction={callback} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('is not checked by default', () => { + // Render a DontKnow in the document + var dontKnow = TestUtils.renderIntoDocument( + <DontKnow checkBoxFunction={noop} /> + ); + + // Get the rendered element + var dontKnowNode = TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input'); + + expect(dontKnowNode.defaultChecked).toEqual(false); + }); + + it('is checked by default when checked property is present', () => { + // Render a DontKnow in the document + var dontKnow = TestUtils.renderIntoDocument( + <DontKnow checkBoxFunction={noop} checked={true} /> + ); + + // Get the rendered element + var dontKnowNode = TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input'); + + expect(dontKnowNode.defaultChecked).toEqual(true); + }); +}); + +describe('PhotoPreview', () => { + + it('calls onClose property when close button is pressed', () => { + var callback = jest.genMockFunction(); + + // Render a DontKnow in the document + var preview = TestUtils.renderIntoDocument( + <PhotoPreview onClose={callback} onDelete={noop} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(preview, 'btn-photo-close') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('calls onDelete property when close button is pressed', () => { + var callback = jest.genMockFunction(); + + // Render a DontKnow in the document + var preview = TestUtils.renderIntoDocument( + <PhotoPreview onClose={noop} onDelete={callback} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(preview, 'btn-photo-delete') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); +}); + +describe('PhotoField', () => { + + it('preview not shown by default', () => { + + // Render a DontKnow in the document + var photo = TestUtils.renderIntoDocument( + <PhotoField /> + ); + + var preview = TestUtils.scryRenderedComponentsWithType(photo, 'PhotoPreview'); + + expect(preview.length).toEqual(0); + }); + + it('preview shown when showPreview prop is true', () => { + + // Render a DontKnow in the document + var photo = TestUtils.renderIntoDocument( + <PhotoField /> + ); + + var preview = TestUtils.scryRenderedComponentsWithType(photo, 'PhotoPreview'); + + expect(preview.length).toEqual(0); + + photo.setState({ + showPreview: true + }); + + jest.runAllTicks(); + + TestUtils.findRenderedComponentWithType(photo, 'PhotoPreview'); + }); +}); + +// describe('Menu', () => { +// var survey = { +// languages: ['English', 'French'] +// }, +// surveyId = 0, +// db; +// beforeEach(function() { +// // Set up some mocked out file info before each test +// db = require('pouchdb'); +// }); + +// it('renders log out/in and revisit reload only when online', () => { +// navigator.onLine = false; + +// var menu = TestUtils.renderIntoDocument( +// <Menu +// language='English' +// survey={survey} +// surveyID={surveyId} +// db={db} +// loggedIn={false} +// hasFacilities={false} +// /> +// ); + +// var loginBtn = TestUtils.scryRenderedDOMComponentsWithClass(menu, 'menu_login'); +// var logoutBtn = TestUtils.scryRenderedDOMComponentsWithClass(menu, 'menu_logout'); + +// expect(loginBtn.length).toEqual(0); +// expect(logoutBtn.length).toEqual(0); + +// }); + +// it('renders log out when user is logged in', () => { + +// }); + +// it('renders log in when user is not logged in', () => { + +// }); + +// it('calls logout function when logout is pressed', () => { + +// }); + +// it('calls login function when login is pressed', () => { + +// }); + +// }); diff --git a/tests/js/example-test.js b/tests/js/example-test.js deleted file mode 100644 index 35dc3e31..00000000 --- a/tests/js/example-test.js +++ /dev/null @@ -1,16 +0,0 @@ -//TODO: deleteme! -// jest.dontMock('../../dokomoforms/static/src/survey/js/components/Header'); - -import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; - -// const Header = require('../../dokomoforms/static/src/survey/js/components/Header'); - -describe('Header', () => { - - it('does something', () => { - expect(3).toEqual(3); - }); - -}); From ab12dc45b82c04a11c6555eb1eb7f39edb1c794c Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Wed, 18 Nov 2015 15:48:40 -0500 Subject: [PATCH 04/69] updated readme --- README.md | 85 ++++++++++++++++++------------------------------------- 1 file changed, 27 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index c12fe43d..4198bbcd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# About +# Dokomo Forms -Dokomo Forms is a self-hosted data collection and analysis platform, and is the successor to [Formhub](https://formhub.org/). +Dokomo Forms is a free and open source data collection and analysis platform. and is the successor to [Formhub](https://formhub.org/). [![Build Status](https://travis-ci.org/SEL-Columbia/dokomoforms.svg?branch=master)](https://travis-ci.org/SEL-Columbia/dokomoforms) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SEL-Columbia/dokomoforms?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -13,78 +13,47 @@ Dokomo Forms is a self-hosted data collection and analysis platform, and is the [![Documentation Status](https://readthedocs.org/projects/dokomoforms/badge/?version=latest)](https://readthedocs.org/projects/dokomoforms/?badge=latest) -# Staging +## About the Project -1. Organization owns instance, and all users belong to the organization. (TODO) +Several solutions exist to handle offline mobile data collection. While this type of technology is increasingly valuable to organizations working in the developing world, the available solutions are cumbersome to use, often requiring a confusing mélange of individual software components and advanced technical skills to setup and manage. -2. Filesystem-level encryption. (TODO) +**Dokomo strives to simplify the process by integrating the elements of a data collection effort into a unified system, from creation of mobile-ready surveys to quick analysis and visualization of the collected data.** -3. i18n +## Features -4. Focus on questions rather than surveys. (TODO) +#### Mobile-Web Technology -5. You can specify configuration options in `local_config.py` or as command line flags to [webapp.py](webapp.py). The available options are defined in [dokomoforms/options.py](dokomoforms/options.py) +Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. -6. [webapp.py](webapp.py) now sets up the tables for you (no more `manage_db.py`). If you want `$ manage_db.py -d`, run +#### Survey Monitoring - `$ ./webapp.py --kill=True` +As an adminstrator of a surveying effort, it's important to know where, when, and by whom data is being submitted. Dokomo Forms lets administrators quickly see the current progress of an effort, providing a quick list and map of the latest submissions and a graph showing submissions/day for the recent past. - You can also specify the schema you want like `$ ./webappy.py --schema=whatever` +#### Revisit Integration -7. New way to run tests (after `$ pip install tox`): +Dokomo Forms integrates with [Revisit](http://revisit.global), a global facility registry API built here at the Sustainable Engineering Lab. Leveraging Revisit, multiple surveys conducted at the same facility can be easily linked, enabling longitudinal studies and allowing progress towards targets be tracked in collected indicators. - `$ tox` +## Under Development - Or, if you want the coverage report as well, +Dokomo Forms is under active development, with some pretty nifty features on the horizon. - `$ tox -e cover` +#### Survey Creation GUI - The tests only touch the `doko_test` schema (which they create/destroy for you). +Soon survey administrators will be able to quickly create surveys though a web-based creation tool, built directly into Dokomo Forms. -# Using Docker for Local Dev Environment and Deployment +#### Better Survey Administration -[Docker](https://en.wikipedia.org/wiki/Docker_(software)) is a container management software that aims at component separation and deployment automation. Please refer to [the Docker API](https://docs.docker.com/) for a fuller introduction. +- Publish surveys directly from the administration panel to enumerator's mobile devices. +- Send updates and communications to enumerators -## Using Docker Manually (Docker knowledge required) +#### Data Visualization -There is a [Dockerfile](Dockerfile) in the root directory to build the Docker image of the Dokomo Forms webapp component building on top a Python 3 image. To build the webapp image, run +- View collected data on map +- See quick statistics and aggregations on a per-question basis -> $ docker build -t selcolumbia/dokomoforms . +## Guides and Documentation -However, Dokomo Forms as a service needs other components such as the database in order to work. We have referenced `mdillon/postgis` as the image, since we are using PostgreSQL with the PostGIS extension. You may also substitute `mdillon/postgis` with any image includes PostGIS. A manual way to run Dokomo Forms as a service would involve starting the `postgis` container and linking it to the Dokomo Forms image we have just built, such as: - -> $ docker run -d -p 8888:8888 --link postgis:db selcolumbia/dokomoforms - -## Using Docker for Local Development - -`docker-compose` is the program that automates Docker container building, running, and linking as described above. It uses the [docker-compose.yml](docker-compose.yml) file which is provided in the root directory. - -To start the service locally, run: - -> $ docker-compose up - -Docker will download the necessary images, then build and link them. This step takes 3-5 minutes for the first build. Once the command has finished, you can visit [http://localhost:8888](http://localhost:8888) and start using Dokomo Forms. - -## Using Docker for Automated Deployment - -`docker-machine` is the program that automates the deployment process. It can hook into many VPS providers such as [AWS](http://aws.amazon.com/), [Rackspace](http://www.rackspace.com/) and [DigitalOcean](https://www.digitalocean.com/). - -Here is an example using DigitalOcean: - -1. Obtain a token from DigitalOcean. Click on "Generate New Token" from the API page as indicated below. - - ![doapi](http://i.imgur.com/0SrmqX7.jpg) - -2. Create a droplet with the token you have just acquired - - > $ docker-machine create -d digitalocean --digitalocean-access-token YOUR_ACCESS_TOKEN dokomoforms - -3. Make your local Docker environment aware of this new machine - - > $ eval $(docker-machine env dokomoforms) - -4. Run `docker-compose` with the new environment - - > $ docker-compose up -d - -Now you have an instance of Dokomo Forms running on your DigitalOcean droplet! +- [User Guide](https://github.com/SEL-Columbia/dokomoforms/wiki/User-Guide) +- [Local Development Environment Setup](https://github.com/SEL-Columbia/dokomoforms/wiki/Local-Development-Environment) +- [Deployment](https://github.com/SEL-Columbia/dokomoforms/wiki/Deployment) +- [REST API Documentation](https://github.com/SEL-Columbia/dokomoforms/wiki/REST-API-v0.2.0) \ No newline at end of file From 25dec6b4e0fd940382773e1be777e540281ed2f9 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 14:06:50 -0500 Subject: [PATCH 05/69] WIP - tests --- tests/js/base-component-tests.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/js/base-component-tests.js b/tests/js/base-component-tests.js index 6b65926c..6a6eb33b 100644 --- a/tests/js/base-component-tests.js +++ b/tests/js/base-component-tests.js @@ -294,17 +294,28 @@ describe('PhotoPreview', () => { }); describe('PhotoField', () => { + var sr; - it('preview not shown by default', () => { + beforeEach(function() { + sr = TestUtils.createRenderer(); + }); + + it('does not show preview by default', () => { // Render a DontKnow in the document - var photo = TestUtils.renderIntoDocument( + sr.render( <PhotoField /> ); - var preview = TestUtils.scryRenderedComponentsWithType(photo, 'PhotoPreview'); + var result = sr.getRenderOutput(); - expect(preview.length).toEqual(0); + expect(result.type).toBe('span'); + + console.log(result.props.children[1]); + // expect(result.props.children).toEqual([ + + // ]); + // expect(preview.length).toEqual(0); }); it('preview shown when showPreview prop is true', () => { From f91369c3434854a0e4299875aa085ffd9dee7296 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 14:14:38 -0500 Subject: [PATCH 06/69] fixed content policy issue for revisit prod domain --- dokomoforms/handlers/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dokomoforms/handlers/util.py b/dokomoforms/handlers/util.py index e52ffca0..b821fd07 100644 --- a/dokomoforms/handlers/util.py +++ b/dokomoforms/handlers/util.py @@ -113,7 +113,8 @@ def set_default_headers(self): "img-src 'self' *.tile.openstreetmap.org data: blob:;" "object-src 'self' blob:;" "media-src 'self' blob: mediastream:;" - "connect-src 'self' blob: *.revisit.global localhost:3000;" + "connect-src 'self' blob: revisit.global *.revisit.global" + " localhost:3000;" "default-src 'self';" ) From 706e4aa5de49420c43531397008779748c6b5383 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 14:23:56 -0500 Subject: [PATCH 07/69] fixed date formatting [Finishes #108562106] --- dokomoforms/static/src/admin/js/account-overview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dokomoforms/static/src/admin/js/account-overview.js b/dokomoforms/static/src/admin/js/account-overview.js index 4b1a0745..44de3a71 100644 --- a/dokomoforms/static/src/admin/js/account-overview.js +++ b/dokomoforms/static/src/admin/js/account-overview.js @@ -58,7 +58,7 @@ var AccountOverview = (function() { var submissions = data.submissions, $table = $('#recent-list table tbody'); submissions.forEach(function(sub) { - sub.submission_time = moment(sub.submission_time).format('MMM d, YYYY [at] HH:mm'); + sub.submission_time = moment(sub.submission_time).format('MMM D, YYYY [at] HH:mm'); sub.survey = { id: sub.survey_id, default_language: sub.survey_default_language From 0b4a70137fb42c4dab18f40739806464b89272a3 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 15:40:34 -0500 Subject: [PATCH 08/69] added images to Readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4198bbcd..5e48eb14 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,14 @@ Instead of relying on platform-specific apps, Dokomo's surveys are conducted usi As an adminstrator of a surveying effort, it's important to know where, when, and by whom data is being submitted. Dokomo Forms lets administrators quickly see the current progress of an effort, providing a quick list and map of the latest submissions and a graph showing submissions/day for the recent past. +![alt Dokomo Forms Admin - Manage](https://i.imgur.com/6z7UJt2.jpg) + +#### Submission Data Quick Views + +Administrators can quickly view data from individual submissions and get some basic statistics and aggregations from each question on a survey. + +![alt Dokomo Forms Admin - Data](https://i.imgur.com/hwYRf8e.jpg) + #### Revisit Integration Dokomo Forms integrates with [Revisit](http://revisit.global), a global facility registry API built here at the Sustainable Engineering Lab. Leveraging Revisit, multiple surveys conducted at the same facility can be easily linked, enabling longitudinal studies and allowing progress towards targets be tracked in collected indicators. @@ -56,4 +64,4 @@ Soon survey administrators will be able to quickly create surveys though a web-b - [User Guide](https://github.com/SEL-Columbia/dokomoforms/wiki/User-Guide) - [Local Development Environment Setup](https://github.com/SEL-Columbia/dokomoforms/wiki/Local-Development-Environment) - [Deployment](https://github.com/SEL-Columbia/dokomoforms/wiki/Deployment) -- [REST API Documentation](https://github.com/SEL-Columbia/dokomoforms/wiki/REST-API-v0.2.0) \ No newline at end of file +- [REST API Documentation](https://github.com/SEL-Columbia/dokomoforms/wiki/REST-API-v0.2.0) From 611f865cbc501bb34c0bfb8af9941940106e25a3 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Thu, 19 Nov 2015 15:42:01 -0500 Subject: [PATCH 09/69] No need to expose port 8888 of the host machine --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index de4ef25b..55e7303a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,6 @@ webapp: command: bash -c "head -c 24 /dev/urandom > cookie_secret && python webapp.py" links: - "db:db" - ports: - - "8888:8888" volumes: - ./local_config.py:/dokomo/local_config.py db: From 2331c00d161f630456dfc696e4e0e0eee9db439a Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 16:21:09 -0500 Subject: [PATCH 10/69] more images of survey on phone --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e48eb14..674c0aa6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ Several solutions exist to handle offline mobile data collection. While this typ #### Mobile-Web Technology -Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. +Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. This makes for an easier workflow for enumerators and administrators — surveys can be accessed on anywhere via a normal web link, and can be conducted on (almost) any device. + +![alt Dokomo Forms Admin - Manage](https://i.imgur.com/saW5zcB.jpg) #### Survey Monitoring From d9418a73d58559c058713d3a58af722ba1f71a1e Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 16:26:29 -0500 Subject: [PATCH 11/69] typo fixes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 674c0aa6..280b3cc0 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Several solutions exist to handle offline mobile data collection. While this typ #### Mobile-Web Technology -Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. This makes for an easier workflow for enumerators and administrators — surveys can be accessed on anywhere via a normal web link, and can be conducted on (almost) any device. +Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. This makes for an easier workflow for enumerators and administrators — surveys can be accessed via a normal web link, and can be conducted on (almost) any device that has a web browser. ![alt Dokomo Forms Admin - Manage](https://i.imgur.com/saW5zcB.jpg) @@ -41,7 +41,7 @@ Administrators can quickly view data from individual submissions and get some ba #### Revisit Integration -Dokomo Forms integrates with [Revisit](http://revisit.global), a global facility registry API built here at the Sustainable Engineering Lab. Leveraging Revisit, multiple surveys conducted at the same facility can be easily linked, enabling longitudinal studies and allowing progress towards targets be tracked in collected indicators. +Dokomo Forms integrates with [Revisit](http://revisit.global), a global facility registry API built here at the Sustainable Engineering Lab. Leveraging Revisit, multiple surveys conducted at the same facility can be easily linked, allowing changes in data points at survey locations to be tracked over time. ## Under Development @@ -53,7 +53,7 @@ Soon survey administrators will be able to quickly create surveys though a web-b #### Better Survey Administration -- Publish surveys directly from the administration panel to enumerator's mobile devices. +- Publish surveys directly from the administration panel to enumerators' mobile devices. - Send updates and communications to enumerators #### Data Visualization From 5d5de4a72567356ceb529ae3b474f829ddee57c9 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 16:44:07 -0500 Subject: [PATCH 12/69] fixed typo, condensed badges --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 280b3cc0..88d93af0 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ # Dokomo Forms -Dokomo Forms is a free and open source data collection and analysis platform. and is the successor to [Formhub](https://formhub.org/). +Dokomo Forms is a free and open source data collection and analysis platform. [![Build Status](https://travis-ci.org/SEL-Columbia/dokomoforms.svg?branch=master)](https://travis-ci.org/SEL-Columbia/dokomoforms) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SEL-Columbia/dokomoforms?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - [![Coverage Status](https://coveralls.io/repos/SEL-Columbia/dokomoforms/badge.svg?branch=master)](https://coveralls.io/r/SEL-Columbia/dokomoforms?branch=master) - [![Sauce Test Status](https://saucelabs.com/browser-matrix/dokomo_sauce_matrix.svg)](https://saucelabs.com/u/dokomo_sauce_matrix) - [![Dependency Status](https://gemnasium.com/SEL-Columbia/dokomoforms.svg)](https://gemnasium.com/SEL-Columbia/dokomoforms) - [![Documentation Status](https://readthedocs.org/projects/dokomoforms/badge/?version=latest)](https://readthedocs.org/projects/dokomoforms/?badge=latest) ## About the Project From dd652c631c29b7ad6c5126e7735cb52c9c06abf8 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 19 Nov 2015 16:45:08 -0500 Subject: [PATCH 13/69] reorder badges --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 88d93af0..d12640c1 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ Dokomo Forms is a free and open source data collection and analysis platform. [![Build Status](https://travis-ci.org/SEL-Columbia/dokomoforms.svg?branch=master)](https://travis-ci.org/SEL-Columbia/dokomoforms) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SEL-Columbia/dokomoforms?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Coverage Status](https://coveralls.io/repos/SEL-Columbia/dokomoforms/badge.svg?branch=master)](https://coveralls.io/r/SEL-Columbia/dokomoforms?branch=master) -[![Sauce Test Status](https://saucelabs.com/browser-matrix/dokomo_sauce_matrix.svg)](https://saucelabs.com/u/dokomo_sauce_matrix) -[![Dependency Status](https://gemnasium.com/SEL-Columbia/dokomoforms.svg)](https://gemnasium.com/SEL-Columbia/dokomoforms) [![Documentation Status](https://readthedocs.org/projects/dokomoforms/badge/?version=latest)](https://readthedocs.org/projects/dokomoforms/?badge=latest) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SEL-Columbia/dokomoforms?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Dependency Status](https://gemnasium.com/SEL-Columbia/dokomoforms.svg)](https://gemnasium.com/SEL-Columbia/dokomoforms) +[![Sauce Test Status](https://saucelabs.com/browser-matrix/dokomo_sauce_matrix.svg)](https://saucelabs.com/u/dokomo_sauce_matrix) ## About the Project From 761720ad993fc5cc38b81d0bfc99689538bf17f6 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Fri, 20 Nov 2015 10:20:48 -0500 Subject: [PATCH 14/69] minor clarification --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d12640c1..8810f46f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Several solutions exist to handle offline mobile data collection. While this typ #### Mobile-Web Technology -Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. This makes for an easier workflow for enumerators and administrators — surveys can be accessed via a normal web link, and can be conducted on (almost) any device that has a web browser. +Instead of relying on platform-specific apps, Dokomo's surveys are conducted using an offline-capable mobile web app. This makes for an easier workflow for administrators and enumerators — surveys can be distributed and accessed via a normal web link, and can be conducted on (almost) any device that has a web browser. ![alt Dokomo Forms Admin - Manage](https://i.imgur.com/saW5zcB.jpg) From 420b6a7a9d0f1f914bb61ced7b3059b39c41dea3 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Fri, 20 Nov 2015 11:26:50 -0500 Subject: [PATCH 15/69] The webapp Docker container will now wait for Postgres when using docker-compose --- Dockerfile | 2 +- docker-compose-dev.yml | 2 +- docker-compose.yml | 2 +- docker-wait-for-postgres.sh | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100755 docker-wait-for-postgres.sh diff --git a/Dockerfile b/Dockerfile index d1bf024e..320f278b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.4 WORKDIR /dokomo -RUN apt-get update && apt-get install npm nodejs -y +RUN apt-get update && apt-get install npm nodejs postgresql-client -y ADD package.json /tmp/package.json RUN cd /tmp && npm install && npm install lodash --save-dev RUN cp -a /tmp/node_modules /dokomo/ diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d0f265bb..13072eab 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,6 +1,6 @@ webapp-dev: build: . - command: bash -c "head -c 24 /dev/urandom > cookie_secret && python webapp.py" + command: bash -c "./docker-wait-for-postgres.sh db-dev && head -c 24 /dev/urandom > cookie_secret && python webapp.py" volumes: - ./:/dokomo links: diff --git a/docker-compose.yml b/docker-compose.yml index 55e7303a..900ba628 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ nginx: - ./nginx.conf:/etc/nginx/nginx.conf webapp: image: "selcolumbia/dokomoforms" - command: bash -c "head -c 24 /dev/urandom > cookie_secret && python webapp.py" + command: bash -c "./docker-wait-for-postgres.sh db && head -c 24 /dev/urandom > cookie_secret && python webapp.py" links: - "db:db" volumes: diff --git a/docker-wait-for-postgres.sh b/docker-wait-for-postgres.sh new file mode 100755 index 00000000..6c796cb0 --- /dev/null +++ b/docker-wait-for-postgres.sh @@ -0,0 +1,6 @@ +#!/bin/sh +until psql --host=$1 --username=postgres -w &>/dev/null +do + echo "Waiting for PostgreSQL..." + sleep 1 +done From c0875fa6e9554a5dbb9ba14fc11bb5726fc532ef Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Mon, 23 Nov 2015 10:22:00 -0500 Subject: [PATCH 16/69] WIP - react component tests --- .../components/baseComponents/PhotoField.js | 13 - .../baseComponents/ResponseField.js | 114 ++-- tests/js/base-component-tests.js | 616 ++++++++++++++++-- 3 files changed, 643 insertions(+), 100 deletions(-) diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js b/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js index 827a30cc..05c9f54c 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js @@ -20,26 +20,13 @@ module.exports = React.createClass({ }; }, - /* - * Validate the answer based on props.type - * - * @answer: The response to be validated - * - * TODO: implement photo validation, if necessary... - */ - validate: function(answer) { - return true; - }, - showPreview: function() { - console.log('showPreview'); this.setState({ showPreview: true }); }, hidePreview: function() { - console.log('hidePreview'); this.setState({ showPreview: false }); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js index 7e1fc530..8cfc953b 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js @@ -19,6 +19,12 @@ module.exports = React.createClass({ return {}; }, + getDefaultProps: function() { + return { + logic: {} + }; + }, + // Determine the input field type based on props.type getResponseType: function() { var type = this.props.type; @@ -60,69 +66,83 @@ module.exports = React.createClass({ validate: function(answer) { var type = this.props.type; var logic = this.props.logic; - console.log('Enforcing: ', logic); - var val = null; + + // console.log('logic', logic); + + // assume false + var val; switch (type) { case 'integer': val = parseInt(answer); - if (isNaN(val)) { - val = null; - break; - } - if (logic && logic.min && typeof logic.min === 'number') { - if (val < logic.min) { - val = null; - } - } - - if (logic && logic.max && typeof logic.max === 'number') { - if (val > logic.max) { - val = null; + // in a try/catch to avoid checking logic props + try { + if (isNaN(val) || val < logic.min || val > logic.max) { + return false; } + } catch(ignore) { + // ignore } break; case 'decimal': val = parseFloat(answer); - if (isNaN(val)) { - val = null; - } - if (logic && logic.min && typeof logic.min === 'number') { - if (val < logic.min) { - val = null; - } - } - - if (logic && logic.max && typeof logic.max === 'number') { - if (val > logic.max) { - val = null; + // in a try/catch to avoid checking logic props + try { + if (isNaN(val) || val < logic.min || val > logic.max) { + return false; } + } catch(ignore) { + // ignore } break; case 'date': var resp = new Date(answer); - var day = ('0' + resp.getDate()).slice(-2); - var month = ('0' + (resp.getMonth() + 1)).slice(-2); - var year = resp.getFullYear(); - val = answer; //XXX Keep format? - if (isNaN(year) || isNaN(month) || isNaN(day)) { - val = null; + // var day = ('0' + resp.getDate()).slice(-2); + // var month = ('0' + (resp.getMonth() + 1)).slice(-2); + // var year = resp.getFullYear(); + // val = answer; //XXX Keep format? + + var min = new Date(logic.min); + var max = new Date(logic.max); + + console.log(resp, logic.min, min, logic.max, max); + // make sure min / max are parseable dates + if (isNaN(min) || isNaN(max)) { + return false; } - if (logic && logic.min && !isNaN((new Date(logic.min)).getDate())) { - if (resp < new Date(logic.min)) { - val = null; - } + // validate response + if (resp < min || resp > max) { + return false; } - if (logic && logic.max && !isNaN((new Date(logic.max)).getDate())) { - if (resp > new Date(logic.max)) { - val = null; - } - } + // // in a try/catch to avoid checking logic props + // try { + // if (isNaN(val) || val < logic.min || val > logic.max) { + // return false; + // } + // } catch(ignore) { + // // ignore + // } + + // if (isNaN(year) || isNaN(month) || isNaN(day)) { + // return false; + // } + + // if (logic && logic.min && !isNaN((new Date(logic.min)).getDate())) { + // if (resp < new Date(logic.min)) { + // return false; + // } + // } + + // if (logic && logic.max && !isNaN((new Date(logic.max)).getDate())) { + // if (resp > new Date(logic.max)) { + // return false; + // } + // } break; case 'timestamp': @@ -134,8 +154,7 @@ module.exports = React.createClass({ } } - return val; - + return true; }, /* @@ -145,11 +164,13 @@ module.exports = React.createClass({ * @event: Change event */ onChange: function(event) { - var value = this.validate(event.target.value); + var value = event.target.value; var input = event.target; + var isValid = this.validate(value); + input.setCustomValidity(''); - if (value === null) { + if (!isValid) { window.target = event.target; input.setCustomValidity('Invalid field.'); } @@ -158,6 +179,7 @@ module.exports = React.createClass({ this.props.onInput(value, this.props.index); }, + // TODO: invalid HTML, input field should not have children -- refactor to move span outside input. render: function() { return ( <div className='input_container'> diff --git a/tests/js/base-component-tests.js b/tests/js/base-component-tests.js index 6a6eb33b..c8ae8c13 100644 --- a/tests/js/base-component-tests.js +++ b/tests/js/base-component-tests.js @@ -2,30 +2,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Card.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Message.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Title.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); -jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js'); -// jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Menu.js'); - -const BigButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton'); -const LittleButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton'); -const Card = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Card'); -const Message = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Message'); -const Title = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Title'); -const DontKnow = require('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow'); -const PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); -const PhotoField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField'); -// const Menu = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Menu'); - // a noop function useful for passing into components that require it. var noop = () => {}; describe('BigButton', () => { + var BigButton; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js'); + BigButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton'); + }); it('renders displaying its text property', () => { // Render a BigButton in the document @@ -70,6 +56,12 @@ describe('BigButton', () => { }); describe('LittleButton', () => { + var LittleButton; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js'); + LittleButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton'); + }); it('renders displaying its text property', () => { // Render a LittleButton in the document @@ -131,6 +123,12 @@ describe('LittleButton', () => { }); describe('Card', () => { + var Card; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Card.js'); + Card = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Card'); + }); it('should render a sinlge message', () => { var message = ['hello']; @@ -169,6 +167,12 @@ describe('Card', () => { }); describe('Message', () => { + var Message; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Message.js'); + Message = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Message'); + }); it('should render a sinlge message', () => { var message = TestUtils.renderIntoDocument( @@ -196,6 +200,12 @@ describe('Message', () => { }); describe('Title', () => { + var Title; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Title.js'); + Title = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Title'); + }); it('should render a title with a message', () => { var title = TestUtils.renderIntoDocument( @@ -213,6 +223,12 @@ describe('Title', () => { describe('DontKnow', () => { + var DontKnow; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js'); + DontKnow = require('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow'); + }); it('calls the checkBoxFunction property when pressed', () => { var callback = jest.genMockFunction(); @@ -257,6 +273,12 @@ describe('DontKnow', () => { }); describe('PhotoPreview', () => { + var PhotoPreview; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); + PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); + }); it('calls onClose property when close button is pressed', () => { var callback = jest.genMockFunction(); @@ -294,49 +316,561 @@ describe('PhotoPreview', () => { }); describe('PhotoField', () => { - var sr; + var PhotoField; beforeEach(function() { - sr = TestUtils.createRenderer(); + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js'); + PhotoField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField'); }); - it('does not show preview by default', () => { + it('calls showPreview on thumbnail click', () => { - // Render a DontKnow in the document - sr.render( + // Hackity hack hack hack + // https://github.com/facebook/jest/issues/207 + PhotoField.prototype.__reactAutoBindMap.showPreview = jest.genMockFunction(); + + var Photo = TestUtils.renderIntoDocument( <PhotoField /> ); - var result = sr.getRenderOutput(); + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); - expect(result.type).toBe('span'); + expect(PhotoField.prototype.__reactAutoBindMap.showPreview).toBeCalled(); + }); + + it('renders image tag when initValue passed', () => { + + var Photo = TestUtils.renderIntoDocument( + <PhotoField initValue='nothing.jpg' /> + ); - console.log(result.props.children[1]); - // expect(result.props.children).toEqual([ + TestUtils.findRenderedDOMComponentWithTag(Photo, 'img'); - // ]); - // expect(preview.length).toEqual(0); }); - it('preview shown when showPreview prop is true', () => { + it('renders PhotoPreview on thumbnail click', () => { - // Render a DontKnow in the document - var photo = TestUtils.renderIntoDocument( + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); + var PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); + + var Photo = TestUtils.renderIntoDocument( <PhotoField /> ); - var preview = TestUtils.scryRenderedComponentsWithType(photo, 'PhotoPreview'); + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); + }); + + + + it('hides PhotoPreview on thumbnail click', () => { + + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); + var PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); + + // render photo field instance + var Photo = TestUtils.renderIntoDocument( + <PhotoField /> + ); + + // click thumbnail + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + // get reference to rendered PhotoPreview instance + var PhotoPreviewInstance = TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); + + // click close button on preview + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(PhotoPreviewInstance, 'btn-photo-close') + ); + + // check that there are no longer any rendered PhotoPreview instances + var PhotoPreviewInstances = TestUtils.scryRenderedComponentsWithType(Photo, PhotoPreview); - expect(preview.length).toEqual(0); + expect(PhotoPreviewInstances.length).toEqual(0); + }); + + it('calls onDelete when delete button pressed in preview', () => { + + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); + var PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); + + var callback = jest.genMockFunction(); + + // render photo field instance + var Photo = TestUtils.renderIntoDocument( + <PhotoField buttonFunction={callback} /> + ); + + // click thumbnail + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + // get reference to rendered PhotoPreview instance + var PhotoPreviewInstance = TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); + + // click close button on preview + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(PhotoPreviewInstance, 'btn-photo-delete') + ); + + // check that the delete method was called (which in turn calls buttonFunction prop) + expect(callback).toBeCalled(); + }); - photo.setState({ - showPreview: true - }); +}); - jest.runAllTicks(); +describe('ResponseField', () => { + var ResponseField, callback, setCustomValidity; - TestUtils.findRenderedComponentWithType(photo, 'PhotoPreview'); + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js'); + ResponseField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField'); + callback = jest.genMockFunction(); + setCustomValidity = jest.genMockFunction(); }); + + it('renders a number input if prop type is integer', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='integer' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('number'); + + // simulate input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 50 + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a number input if prop type is decimal', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='decimal' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('number'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 50 + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a datetime-local input if prop type is timestamp', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='timestamp' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('datetime-local'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 1238592847 + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a time input if prop type is time', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='time' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 1238592847 + } + } + ); + + expect(input.getAttribute('type')).toEqual('time'); + }); + + it('renders a date input if prop type is date', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='date' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('date'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2013-01-01T00:00:00.000Z' + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders an email input if prop type is email', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='email' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('email'); + }); + + it('renders a text input if prop type is not specified', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('text'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'onetwothree' + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a minus if prop showMinus is true', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField showMinus={true} buttonFunction={callback} /> + ); + + // check that minus is rendered + TestUtils.findRenderedDOMComponentWithClass(ResponseFieldInstance, 'question__minus'); + }); + + it('calls buttonFunction when minus is clicked', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField showMinus={true} buttonFunction={callback} /> + ); + + // click on minus + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(ResponseFieldInstance, 'question__minus') + ); + + // check that the delete method was called (which in turn calls buttonFunction prop) + expect(callback).toBeCalled(); + }); + + it('calls onInput prop via onChange when input changes', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: noop, + value: 'a' + } + } + ); + + // check that the delete method was called (which in turn calls buttonFunction prop) + expect(callback).toBeCalled(); + }); + + it('validates non-numeric values in integer field', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='integer' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'a' + } + } + ); + + // called once with empty string to clear, then again with invalid message + expect(setCustomValidity.mock.calls[0][0]).toEqual(''); + expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); + }); + + it('validates min and max values in integer field', () => { + + var logic = { + min: 10, + max: 20 + }; + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='integer' logic={logic} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate correct input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 15 + } + } + ); + + // should be only called once if validation passes + expect(setCustomValidity.mock.calls.length).toEqual(1); + + // simulate bad min input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 5 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(3); + + // simulate bad max input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 25 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(5); + }); + + it('validates non-numeric values in decimal field', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='decimal' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'a' + } + } + ); + + // called once with empty string to clear, then again with invalid message + expect(setCustomValidity.mock.calls[0][0]).toEqual(''); + expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); + }); + + it('validates min and max values in decimal field', () => { + + var logic = { + min: 10, + max: 20 + }; + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='decimal' logic={logic} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate correct input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 15 + } + } + ); + + // should be only called once if validation passes + expect(setCustomValidity.mock.calls.length).toEqual(1); + + // simulate bad min input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 5 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(3); + + // simulate bad max input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 25 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(5); + }); + + it('validates non-parseable date values in date field', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='date' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'gibberish' + } + } + ); + + // called once with empty string to clear, then again with invalid message + expect(setCustomValidity.mock.calls[0][0]).toEqual(''); + expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); + }); + + it('validates min and max values in date field', () => { + + var logic = { + min: '2014-01-01T00:00:00.000Z', + max: '2015-01-01T00:00:00.000Z' + }; + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='decimal' logic={logic} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate correct input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2014-06-01T00:00:00.000Z' + } + } + ); + + // should be only called once if validation passes + expect(setCustomValidity.mock.calls.length).toEqual(1); + + // simulate bad min input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2013-01-01T00:00:00.000Z' + } + } + ); + + // should be called two more times when validation doesn't pass + expect(setCustomValidity.mock.calls.length).toEqual(3); + + // simulate bad max input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2016-01-01T00:00:00.000Z' + } + } + ); + + // should be called two more times when validation doesn't pass + expect(setCustomValidity.mock.calls.length).toEqual(5); + }); + }); // describe('Menu', () => { From 70e64adae3f5d60af694e5a1ced794263f3606b8 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Mon, 23 Nov 2015 14:28:17 -0500 Subject: [PATCH 17/69] more tests more tests --- .../src/survey/js/components/Question.js | 52 +++--- .../baseComponents/ResponseField.js | 114 +++++------- tests/js/base-component-tests.js | 166 +++++++++++++++++- 3 files changed, 231 insertions(+), 101 deletions(-) diff --git a/dokomoforms/static/src/survey/js/components/Question.js b/dokomoforms/static/src/survey/js/components/Question.js index 287ffc41..52b7d7af 100644 --- a/dokomoforms/static/src/survey/js/components/Question.js +++ b/dokomoforms/static/src/survey/js/components/Question.js @@ -20,14 +20,14 @@ module.exports = React.createClass({ var answers = survey[this.props.question.id] || []; var length = answers.length === 0 ? 1 : answers.length; - return { - questionCount: length, - } + return { + questionCount: length + }; }, /* * Hack to force react to update child components - * Gets called by parent element through 'refs' when state of something changed + * Gets called by parent element through 'refs' when state of something changed * (usually localStorage) */ update: function() { @@ -35,7 +35,7 @@ module.exports = React.createClass({ var answers = survey[this.props.question.id] || []; var length = answers.length === 0 ? 1 : answers.length; this.setState({ - questionCount: length, + questionCount: length }); }, @@ -47,13 +47,12 @@ module.exports = React.createClass({ var answers = survey[this.props.question.id] || []; var length = answers.length; - console.log("Length:", length, "Count", this.state.questionCount); - if (answers[length] && answers[length].response_type - || length > 0 && length == this.state.questionCount) { + console.log('Length:', length, 'Count', this.state.questionCount); + if (answers[length] && answers[length].response_type || length > 0 && length === this.state.questionCount) { this.setState({ questionCount: this.state.questionCount + 1 - }) + }); } }, @@ -61,14 +60,13 @@ module.exports = React.createClass({ * Remove input and update localStorage */ removeInput: function(index) { - console.log("Remove", index); + console.log('Remove', index); if (!(this.state.questionCount > 1)) return; var survey = JSON.parse(localStorage[this.props.surveyID] || '{}'); var answers = survey[this.props.question.id] || []; - var length = answers.length === 0 ? 1 : answers.length; answers.splice(index, 1); survey[this.props.question.id] = answers; @@ -77,25 +75,24 @@ module.exports = React.createClass({ this.setState({ questionCount: this.state.questionCount - 1 - }) + }); //this.forceUpdate(); }, /* * Record new response into localStorage, response has been validated - * if this callback is fired + * if this callback is fired */ onInput: function(value, index) { - console.log("Hey", index, value); + console.log('Hey', index, value); var survey = JSON.parse(localStorage[this.props.surveyID] || '{}'); var answers = survey[this.props.question.id] || []; - var length = answers.length === 0 ? 1 : answers.length; //XXX Null value implies failed validation answers[index] = { - 'response': value, + 'response': value, 'response_type': 'answer' }; @@ -113,43 +110,44 @@ module.exports = React.createClass({ * @index: The location in the answer array in localStorage to search */ getAnswer: function(index) { - console.log("In:", index); + console.log('In:', index); var survey = JSON.parse(localStorage[this.props.surveyID] || '{}'); var answers = survey[this.props.question.id] || []; - var length = answers.length === 0 ? 1 : answers.length; console.log(answers, index); return answers[index] && answers[index].response || null; }, render: function() { - var children = Array.apply(null, {length: this.state.questionCount}) + var children = Array.apply(null, { + length: this.state.questionCount + }); var self = this; return ( - <span> + <span> {children.map(function(child, idx) { return ( - <ResponseField + <ResponseField buttonFunction={self.removeInput} onInput={self.onInput} type={self.props.questionType} logic={self.props.question.logic} - key={Math.random()} - index={idx} + key={Math.random()} + index={idx} disabled={self.props.disabled} - initValue={self.getAnswer(idx)} + initValue={self.getAnswer(idx)} showMinus={self.state.questionCount > 1} /> - ) + ); })} {this.props.question.allow_multiple ? <LittleButton buttonFunction={this.addNewInput} disabled={this.props.disabled} text={'add another answer'} /> - : null + : null } </span> - ) + ); } }); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js index 8cfc953b..c74437b7 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js @@ -19,12 +19,6 @@ module.exports = React.createClass({ return {}; }, - getDefaultProps: function() { - return { - logic: {} - }; - }, - // Determine the input field type based on props.type getResponseType: function() { var type = this.props.type; @@ -66,83 +60,69 @@ module.exports = React.createClass({ validate: function(answer) { var type = this.props.type; var logic = this.props.logic; - - // console.log('logic', logic); - - // assume false - var val; + var val = null; switch (type) { case 'integer': val = parseInt(answer); + if (isNaN(val)) { + val = null; + break; + } - // in a try/catch to avoid checking logic props - try { - if (isNaN(val) || val < logic.min || val > logic.max) { - return false; + if (logic && logic.min && typeof logic.min === 'number') { + if (val < logic.min) { + val = null; + } + } + + if (logic && logic.max && typeof logic.max === 'number') { + if (val > logic.max) { + val = null; } - } catch(ignore) { - // ignore } break; case 'decimal': val = parseFloat(answer); + if (isNaN(val)) { + val = null; + } - // in a try/catch to avoid checking logic props - try { - if (isNaN(val) || val < logic.min || val > logic.max) { - return false; + if (logic && logic.min && typeof logic.min === 'number') { + if (val < logic.min) { + val = null; + } + } + + if (logic && logic.max && typeof logic.max === 'number') { + if (val > logic.max) { + val = null; } - } catch(ignore) { - // ignore } break; case 'date': var resp = new Date(answer); - // var day = ('0' + resp.getDate()).slice(-2); - // var month = ('0' + (resp.getMonth() + 1)).slice(-2); - // var year = resp.getFullYear(); - // val = answer; //XXX Keep format? - - var min = new Date(logic.min); - var max = new Date(logic.max); - - console.log(resp, logic.min, min, logic.max, max); - // make sure min / max are parseable dates - if (isNaN(min) || isNaN(max)) { - return false; + var day = ('0' + resp.getDate()).slice(-2); + var month = ('0' + (resp.getMonth() + 1)).slice(-2); + var year = resp.getFullYear(); + val = answer; //XXX Keep format? + if (isNaN(year) || isNaN(month) || isNaN(day)) { + val = null; } - // validate response - if (resp < min || resp > max) { - return false; + + if (logic && logic.min && !isNaN((new Date(logic.min)).getDate())) { + if (resp < new Date(logic.min)) { + val = null; + } } - // // in a try/catch to avoid checking logic props - // try { - // if (isNaN(val) || val < logic.min || val > logic.max) { - // return false; - // } - // } catch(ignore) { - // // ignore - // } - - // if (isNaN(year) || isNaN(month) || isNaN(day)) { - // return false; - // } - - // if (logic && logic.min && !isNaN((new Date(logic.min)).getDate())) { - // if (resp < new Date(logic.min)) { - // return false; - // } - // } - - // if (logic && logic.max && !isNaN((new Date(logic.max)).getDate())) { - // if (resp > new Date(logic.max)) { - // return false; - // } - // } + if (logic && logic.max && !isNaN((new Date(logic.max)).getDate())) { + if (resp > new Date(logic.max)) { + val = null; + } + } break; case 'timestamp': @@ -154,7 +134,8 @@ module.exports = React.createClass({ } } - return true; + return val; + }, /* @@ -164,13 +145,11 @@ module.exports = React.createClass({ * @event: Change event */ onChange: function(event) { - var value = event.target.value; + var value = this.validate(event.target.value); var input = event.target; - var isValid = this.validate(value); - input.setCustomValidity(''); - if (!isValid) { + if (value === null) { window.target = event.target; input.setCustomValidity('Invalid field.'); } @@ -179,7 +158,6 @@ module.exports = React.createClass({ this.props.onInput(value, this.props.index); }, - // TODO: invalid HTML, input field should not have children -- refactor to move span outside input. render: function() { return ( <div className='input_container'> diff --git a/tests/js/base-component-tests.js b/tests/js/base-component-tests.js index c8ae8c13..4a5f3b98 100644 --- a/tests/js/base-component-tests.js +++ b/tests/js/base-component-tests.js @@ -818,12 +818,12 @@ describe('ResponseField', () => { it('validates min and max values in date field', () => { var logic = { - min: '2014-01-01T00:00:00.000Z', - max: '2015-01-01T00:00:00.000Z' + min: '2014-01-01', + max: '2015-01-01' }; var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='decimal' logic={logic} /> + <ResponseField onInput={callback} type='date' logic={logic} /> ); var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); @@ -834,7 +834,7 @@ describe('ResponseField', () => { { target: { setCustomValidity: setCustomValidity, - value: '2014-06-01T00:00:00.000Z' + value: '2014-06-01' } } ); @@ -848,7 +848,7 @@ describe('ResponseField', () => { { target: { setCustomValidity: setCustomValidity, - value: '2013-01-01T00:00:00.000Z' + value: '2013-01-01' } } ); @@ -862,7 +862,7 @@ describe('ResponseField', () => { { target: { setCustomValidity: setCustomValidity, - value: '2016-01-01T00:00:00.000Z' + value: '2016-01-01' } } ); @@ -873,6 +873,160 @@ describe('ResponseField', () => { }); +describe('ResponseFields', () => { + var ResponseFields, ResponseField; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields.js'); + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js'); + ResponseFields = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields'); + ResponseField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField'); + }); + + it('renders multiple ResponseField components based on childCount prop', () => { + var ResponseFieldsInstance = TestUtils.renderIntoDocument( + <ResponseFields childCount='5' onInput={noop} buttonFunction={noop} type='date'/> + ); + + var ResponseFieldInstances = TestUtils.scryRenderedComponentsWithType(ResponseFieldsInstance, ResponseField); + + expect(ResponseFieldInstances.length).toEqual(5); + }); +}); + +describe('Select', () => { + var Select, ResponseField, callback; + + beforeEach(function() { + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Select.js'); + jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js'); + Select = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Select'); + ResponseField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField'); + callback = jest.genMockFunction(); + }); + + it('renders a single select dropdown', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} /> + ); + + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + expect(select.getAttribute('multiple')).toBeNull(); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + + // expect two, as placeholder is added to top + expect(options.length).toEqual(2); + }); + + it('renders a multi select dropdown', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={true} /> + ); + + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + expect(select.getAttribute('multiple')).toBeDefined(); + }); + + it('renders other option', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} withOther={true} /> + ); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + // expect two, as placeholder is added to top + expect(options.length).toEqual(3); + }); + + it('renders other field when other option selected', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} withOther={true} /> + ); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + TestUtils.Simulate.change( + select, + { + target: { + selectedOptions: [ + options[2] + ] + } + } + ); + + // make sure the response field is rendered + var ResponseFieldInstance = TestUtils.findRenderedComponentWithType(SelectInstance, ResponseField); + }); + + it('calls onSelect prop when changed', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} onSelect={callback} /> + ); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + TestUtils.Simulate.change( + select, + { + target: { + selectedOptions: [ + options[1] + ] + } + } + ); + + expect(callback).toBeCalled(); + + }); + + it('renders with default value from initSelect', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} initSelect={[0]} /> + ); + + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + expect(select.value).toEqual('0'); + }); +}); + // describe('Menu', () => { // var survey = { // languages: ['English', 'French'] From 984f661d69cabb3a74dfcfc1cb68be63fad23dd6 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Mon, 23 Nov 2015 16:56:55 -0500 Subject: [PATCH 18/69] formatting --- .../baseComponents/ResponseField.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js index c74437b7..cec30994 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js @@ -161,23 +161,23 @@ module.exports = React.createClass({ render: function() { return ( <div className='input_container'> - <input - type={this.getResponseType()} - step={this.getResponseStep()} - placeholder={this.props.placeholder || 'Please provide a response.'} - onChange={this.onChange} - defaultValue={this.props.initValue} + <input + type={this.getResponseType()} + step={this.getResponseStep()} + placeholder={this.props.placeholder || 'Please provide a response.'} + onChange={this.onChange} + defaultValue={this.props.initValue} + disabled={this.props.disabled} + > + {this.props.showMinus ? + <span + onClick={this.props.buttonFunction.bind(null, this.props.index)} disabled={this.props.disabled} - > - {this.props.showMinus ? - <span - onClick={this.props.buttonFunction.bind(null, this.props.index)} - disabled={this.props.disabled} - className='icon icon-close question__minus'> - </span> - : null} - </input> - </div> + className='icon icon-close question__minus'> + </span> + : null} + </input> + </div> ); } }); From c33f1129461a3eec89380a5daa58b98687cf224a Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Mon, 23 Nov 2015 17:05:09 -0500 Subject: [PATCH 19/69] Fixes timestamp issue [Finishes #108811582] --- .../baseComponents/ResponseField.js | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js index a38e09de..c3e59943 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js @@ -1,4 +1,5 @@ -var React = require('react'); +var React = require('react'), + moment = require('moment'); /* * ResponseField component @@ -16,14 +17,13 @@ var React = require('react'); */ module.exports = React.createClass({ getInitialState: function() { - return { - }; + return {}; }, // Determine the input field type based on props.type getResponseType: function() { var type = this.props.type; - switch(type) { + switch (type) { case 'integer': case 'decimal': return 'number'; @@ -43,7 +43,7 @@ module.exports = React.createClass({ // Determine the input field step based on props.type getResponseStep: function() { var type = this.props.type; - switch(type) { + switch (type) { case 'decimal': return 'any'; case 'timestamp': @@ -63,7 +63,7 @@ module.exports = React.createClass({ var logic = this.props.logic; console.log('Enforcing: ', logic); var val = null; - switch(type) { + switch (type) { case 'integer': val = parseInt(answer); if (isNaN(val)) { @@ -109,7 +109,7 @@ module.exports = React.createClass({ var month = ('0' + (resp.getMonth() + 1)).slice(-2); var year = resp.getFullYear(); val = answer; //XXX Keep format? - if(isNaN(year) || isNaN(month) || isNaN(day)) { + if (isNaN(year) || isNaN(month) || isNaN(day)) { val = null; } @@ -128,11 +128,14 @@ module.exports = React.createClass({ break; case 'timestamp': case 'time': - //TODO: enforce + //TODO: enforce min/max + val = moment(answer).toDate(); + console.log('val: ', val); + break; default: - if (answer) { - val = answer; - } + if (answer) { + val = answer; + } } return val; @@ -161,7 +164,7 @@ module.exports = React.createClass({ render: function() { return ( - <div className='input_container'> + <div className='input_container'> <input type={this.getResponseType()} step={this.getResponseStep()} @@ -179,6 +182,6 @@ module.exports = React.createClass({ : null} </input> </div> - ); + ); } }); From 92a0fd8be3077e2f42a45d717e0b58941fc0666f Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 23 Nov 2015 21:21:06 -0500 Subject: [PATCH 20/69] moment('hh:mm a') is not a valid Date --- .../src/survey/js/components/baseComponents/ResponseField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js index c3e59943..c769d9c7 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js @@ -127,11 +127,11 @@ module.exports = React.createClass({ break; case 'timestamp': - case 'time': //TODO: enforce min/max val = moment(answer).toDate(); console.log('val: ', val); break; + case 'time': default: if (answer) { val = answer; From af9fa319c0728119529bf38bde410a77a971ac02 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 23 Nov 2015 21:28:20 -0500 Subject: [PATCH 21/69] Reintroduce the JS tests in .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d560114..d55aa49f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,12 +25,12 @@ before_script: - node_modules/gulp/bin/gulp.js dev-build script: -# - npm test + - npm test - xvfb-run --server-args="-screen 0, 1280x1280x16" tests/python/coverage_run.sh after_success: - coveralls -# - npm coveralls + - npm coveralls notifications: email: From 397e3224244fb248b11b568680d199f7865806a7 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Tue, 24 Nov 2015 12:49:16 -0500 Subject: [PATCH 22/69] moved js tests, added stub test files so that coverage reports are more accurate --- .../survey/js/__tests__/Application-tests.js | 22 + .../js/api/__tests__/FacilityAPI-tests.js | 22 + .../survey/js/api/__tests__/PhotoAPI-tests.js | 22 + .../js/components/__tests__/Facility-tests.js | 22 + .../js/components/__tests__/Footer-tests.js | 22 + .../js/components/__tests__/Header-tests.js | 22 + .../js/components/__tests__/Loading-tests.js | 22 + .../js/components/__tests__/Location-tests.js | 22 + .../__tests__/MultipleChoice-tests.js | 22 + .../js/components/__tests__/Note-tests.js | 22 + .../js/components/__tests__/Photo-tests.js | 22 + .../js/components/__tests__/Question-tests.js | 22 + .../js/components/__tests__/Splash-tests.js | 22 + .../js/components/__tests__/Submit-tests.js | 22 + .../__tests__/BigButton-tests.js | 56 + .../baseComponents/__tests__/Card-tests.js | 50 + .../__tests__/DontKnow-tests.js | 56 + .../__tests__/FacilityRadios-tests.js | 20 + .../__tests__/LittleButton-tests.js | 73 ++ .../baseComponents/__tests__/Menu-tests.js | 42 + .../baseComponents/__tests__/Message-tests.js | 39 + .../__tests__/PhotoField-tests.js | 112 ++ .../__tests__/PhotoPreview-tests.js | 49 + .../__tests__/ResponseField-tests.js | 451 +++++++ .../__tests__/ResponseFields-tests.js | 27 + .../baseComponents/__tests__/Select-tests.js | 139 +++ .../baseComponents/__tests__/Title-tests.js | 28 + .../js/services/__tests__/auth-tests.js | 22 + .../js/services/__tests__/location-tests.js | 22 + .../js/services/__tests__/utils-tests.js | 22 + package.json | 46 +- tests/js/README.md | 15 + tests/js/base-component-tests.js | 1079 ----------------- 33 files changed, 1552 insertions(+), 1104 deletions(-) create mode 100644 dokomoforms/static/src/survey/js/__tests__/Application-tests.js create mode 100644 dokomoforms/static/src/survey/js/api/__tests__/FacilityAPI-tests.js create mode 100644 dokomoforms/static/src/survey/js/api/__tests__/PhotoAPI-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Facility-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Footer-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Header-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Loading-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Location-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/MultipleChoice-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Note-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Photo-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Question-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Splash-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/__tests__/Submit-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/BigButton-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Card-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/DontKnow-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/LittleButton-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Menu-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Message-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoField-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoPreview-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseField-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseFields-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Select-tests.js create mode 100644 dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Title-tests.js create mode 100644 dokomoforms/static/src/survey/js/services/__tests__/auth-tests.js create mode 100644 dokomoforms/static/src/survey/js/services/__tests__/location-tests.js create mode 100644 dokomoforms/static/src/survey/js/services/__tests__/utils-tests.js create mode 100644 tests/js/README.md delete mode 100644 tests/js/base-component-tests.js diff --git a/dokomoforms/static/src/survey/js/__tests__/Application-tests.js b/dokomoforms/static/src/survey/js/__tests__/Application-tests.js new file mode 100644 index 00000000..f8e15b8d --- /dev/null +++ b/dokomoforms/static/src/survey/js/__tests__/Application-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Application', () => { + var Application; + + beforeEach(function() { + jest.dontMock('../Application.js'); + Application = require('../Application'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/api/__tests__/FacilityAPI-tests.js b/dokomoforms/static/src/survey/js/api/__tests__/FacilityAPI-tests.js new file mode 100644 index 00000000..56e48149 --- /dev/null +++ b/dokomoforms/static/src/survey/js/api/__tests__/FacilityAPI-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('FacilityAPI', () => { + var FacilityAPI; + + beforeEach(function() { + jest.dontMock('../FacilityAPI.js'); + FacilityAPI = require('../FacilityAPI'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/api/__tests__/PhotoAPI-tests.js b/dokomoforms/static/src/survey/js/api/__tests__/PhotoAPI-tests.js new file mode 100644 index 00000000..78863da8 --- /dev/null +++ b/dokomoforms/static/src/survey/js/api/__tests__/PhotoAPI-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('PhotoAPI', () => { + var PhotoAPI; + + beforeEach(function() { + jest.dontMock('../PhotoAPI.js'); + PhotoAPI = require('../PhotoAPI'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Facility-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Facility-tests.js new file mode 100644 index 00000000..1d2f7af5 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Facility-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Facility', () => { + var Facility; + + beforeEach(function() { + jest.dontMock('../Facility.js'); + Facility = require('../Facility'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Footer-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Footer-tests.js new file mode 100644 index 00000000..65e668c9 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Footer-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Footer', () => { + var Footer; + + beforeEach(function() { + jest.dontMock('../Footer.js'); + Footer = require('../Footer'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Header-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Header-tests.js new file mode 100644 index 00000000..55333087 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Header-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Header', () => { + var Header; + + beforeEach(function() { + jest.dontMock('../Header.js'); + Header = require('../Header'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Loading-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Loading-tests.js new file mode 100644 index 00000000..02b191b1 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Loading-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Loading', () => { + var Loading; + + beforeEach(function() { + jest.dontMock('../Loading.js'); + Loading = require('../Loading'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Location-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Location-tests.js new file mode 100644 index 00000000..c4bed0ec --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Location-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Location', () => { + var Location; + + beforeEach(function() { + jest.dontMock('../Location.js'); + Location = require('../Location'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/MultipleChoice-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/MultipleChoice-tests.js new file mode 100644 index 00000000..3d24373b --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/MultipleChoice-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('MultipleChoice', () => { + var MultipleChoice; + + beforeEach(function() { + jest.dontMock('../MultipleChoice.js'); + MultipleChoice = require('../MultipleChoice'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Note-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Note-tests.js new file mode 100644 index 00000000..47382d7b --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Note-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Note', () => { + var Note; + + beforeEach(function() { + jest.dontMock('../Note.js'); + Note = require('../Note'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Photo-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Photo-tests.js new file mode 100644 index 00000000..f8d11f26 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Photo-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Photo', () => { + var Photo; + + beforeEach(function() { + jest.dontMock('../Photo.js'); + Photo = require('../Photo'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Question-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Question-tests.js new file mode 100644 index 00000000..db1bc3e6 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Question-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Question', () => { + var Question; + + beforeEach(function() { + jest.dontMock('../Question.js'); + Question = require('../Question'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Splash-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Splash-tests.js new file mode 100644 index 00000000..4deab344 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Splash-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Splash', () => { + var Splash; + + beforeEach(function() { + jest.dontMock('../Splash.js'); + Splash = require('../Splash'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/__tests__/Submit-tests.js b/dokomoforms/static/src/survey/js/components/__tests__/Submit-tests.js new file mode 100644 index 00000000..d2cd8d32 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/__tests__/Submit-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Submit', () => { + var Submit; + + beforeEach(function() { + jest.dontMock('../Submit.js'); + Submit = require('../Submit'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/BigButton-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/BigButton-tests.js new file mode 100644 index 00000000..d8026141 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/BigButton-tests.js @@ -0,0 +1,56 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('BigButton', () => { + var BigButton; + + beforeEach(function() { + jest.dontMock('../BigButton.js'); + BigButton = require('../BigButton'); + }); + + it('renders displaying its text property', () => { + // Render a BigButton in the document + var button = TestUtils.renderIntoDocument( + <BigButton text='Press Me!' buttonFunction={noop} /> + ); + + // Get the rendered element + var buttonNode = ReactDOM.findDOMNode(button); + + // Verify that its text matches Press Me! + expect(buttonNode.textContent).toEqual('Press Me!'); + }); + + it('calls the buttonFunction property when pressed', () => { + var callback = jest.genMockFunction(); + + // Render a BigButton in the document + var button = TestUtils.renderIntoDocument( + <BigButton text='Press Me!' buttonFunction={callback} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(button, 'button') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('should add a class associated with a type property', () => { + // Render a BigButton in the document + var button = TestUtils.renderIntoDocument( + <BigButton text='Press Me!' buttonFunction={noop} type='btn-testing' /> + ); + + // makes sure the type property has added the class + TestUtils.findRenderedDOMComponentWithClass(button, 'btn-testing'); + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Card-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Card-tests.js new file mode 100644 index 00000000..d0c85145 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Card-tests.js @@ -0,0 +1,50 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Card', () => { + var Card; + + beforeEach(function() { + jest.dontMock('../Card.js'); + Card = require('../Card'); + }); + + it('should render a sinlge message', () => { + var message = ['hello']; + + var card = TestUtils.renderIntoDocument( + <Card messages={message} /> + ); + + // makes sure there is a single span tag (i.e. message) + TestUtils.findRenderedDOMComponentWithTag(card, 'span'); + }); + + it('should render multiple messages', () => { + var messages = ['hello', 'hi', 'hola']; + + var card = TestUtils.renderIntoDocument( + <Card messages={messages} /> + ); + + // makes sure there is a single span tag (i.e. message) + var spans = TestUtils.scryRenderedDOMComponentsWithTag(card, 'span'); + + expect(spans.length).toEqual(3); + }); + + it('should add a class associated with a type property', () => { + var message = ['hello']; + + var card = TestUtils.renderIntoDocument( + <Card messages={message} type='message-secondary' /> + ); + + // makes sure there the type prop has added the class + TestUtils.findRenderedDOMComponentWithClass(card, 'message-secondary'); + }); +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/DontKnow-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/DontKnow-tests.js new file mode 100644 index 00000000..5356daeb --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/DontKnow-tests.js @@ -0,0 +1,56 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('DontKnow', () => { + var DontKnow; + + beforeEach(function() { + jest.dontMock('../DontKnow.js'); + DontKnow = require('../DontKnow'); + }); + + it('calls the checkBoxFunction property when pressed', () => { + var callback = jest.genMockFunction(); + + // Render a DontKnow in the document + var dontKnow = TestUtils.renderIntoDocument( + <DontKnow checkBoxFunction={callback} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('is not checked by default', () => { + // Render a DontKnow in the document + var dontKnow = TestUtils.renderIntoDocument( + <DontKnow checkBoxFunction={noop} /> + ); + + // Get the rendered element + var dontKnowNode = TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input'); + + expect(dontKnowNode.defaultChecked).toEqual(false); + }); + + it('is checked by default when checked property is present', () => { + // Render a DontKnow in the document + var dontKnow = TestUtils.renderIntoDocument( + <DontKnow checkBoxFunction={noop} checked={true} /> + ); + + // Get the rendered element + var dontKnowNode = TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input'); + + expect(dontKnowNode.defaultChecked).toEqual(true); + }); +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js new file mode 100644 index 00000000..72e5089c --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('FacilityRadios', () => { + var FacilityRadios; + + beforeEach(function() { + jest.dontMock('../FacilityRadios.js'); + FacilityRadios = require('../FacilityRadios'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/LittleButton-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/LittleButton-tests.js new file mode 100644 index 00000000..de4193d9 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/LittleButton-tests.js @@ -0,0 +1,73 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('LittleButton', () => { + var LittleButton; + + beforeEach(function() { + jest.dontMock('../LittleButton.js'); + LittleButton = require('../LittleButton'); + }); + + it('renders displaying its text property', () => { + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + <LittleButton text='Press Me!' buttonFunction={noop} /> + ); + + // Get the rendered element + var buttonNode = ReactDOM.findDOMNode(button); + + // Verify that its text matches Press Me! + expect(buttonNode.textContent).toEqual('Press Me!'); + }); + + it('calls the buttonFunction property when pressed', () => { + var callback = jest.genMockFunction(); + + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + <LittleButton text='Press Me!' buttonFunction={callback} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(button, 'button') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('is disabled when disabled property is set', () => { + var callback = jest.genMockFunction(); + + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + <LittleButton text='Press Me!' buttonFunction={callback} disabled={true} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithTag(button, 'button') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(0); + }); + + it('includes an icon if an icon property is set', () => { + // Render a LittleButton in the document + var button = TestUtils.renderIntoDocument( + <LittleButton text='Press Me!' buttonFunction={noop} icon="icon-test" /> + ); + + // makes sure there is a span tag (i.e. icon) rendered within the button + TestUtils.findRenderedDOMComponentWithTag(button, 'span'); + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Menu-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Menu-tests.js new file mode 100644 index 00000000..dc14e31e --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Menu-tests.js @@ -0,0 +1,42 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Menu', () => { + var Menu; + + beforeEach(function() { + jest.dontMock('../Menu.js'); + Menu = require('../Menu'); + }); + + it('does nothing', () => { + + }); + + // it('renders log out/in and revisit reload only when online', () => { + + // }); + + // it('renders log out when user is logged in', () => { + + // }); + + // it('renders log in when user is not logged in', () => { + + // }); + + // it('calls logout function when logout is pressed', () => { + + // }); + + // it('calls login function when login is pressed', () => { + + // }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Message-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Message-tests.js new file mode 100644 index 00000000..299bf401 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Message-tests.js @@ -0,0 +1,39 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Message', () => { + var Message; + + beforeEach(function() { + jest.dontMock('../Message.js'); + Message = require('../Message'); + }); + + it('should render a sinlge message', () => { + var message = TestUtils.renderIntoDocument( + <Message text='Hello' /> + ); + + // Get the rendered element + var messageNode = ReactDOM.findDOMNode(message); + + // Verify that its text matches Press Me! + expect(messageNode.textContent).toEqual('Hello'); + }); + + + it('should add classes associated with the classes property', () => { + var classes = 'testing'; + + var message = TestUtils.renderIntoDocument( + <Message text='Hello' classes={classes} /> + ); + + // makes sure there the type prop has added the class + TestUtils.findRenderedDOMComponentWithClass(message, 'testing'); + }); +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoField-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoField-tests.js new file mode 100644 index 00000000..9095c4a6 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoField-tests.js @@ -0,0 +1,112 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('PhotoField', () => { + var PhotoField, PhotoPreview; + + beforeEach(function() { + jest.dontMock('../PhotoField.js'); + PhotoField = require('../PhotoField'); + jest.dontMock('../PhotoPreview.js'); + PhotoPreview = require('../PhotoPreview'); + }); + + it('calls showPreview on thumbnail click', () => { + + // Hackity hack hack hack + // https://github.com/facebook/jest/issues/207 + PhotoField.prototype.__reactAutoBindMap.showPreview = jest.genMockFunction(); + + var Photo = TestUtils.renderIntoDocument( + <PhotoField /> + ); + + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + expect(PhotoField.prototype.__reactAutoBindMap.showPreview).toBeCalled(); + }); + + it('renders image tag when initValue passed', () => { + + var Photo = TestUtils.renderIntoDocument( + <PhotoField initValue='nothing.jpg' /> + ); + + TestUtils.findRenderedDOMComponentWithTag(Photo, 'img'); + + }); + + it('renders PhotoPreview on thumbnail click', () => { + + var Photo = TestUtils.renderIntoDocument( + <PhotoField /> + ); + + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); + }); + + + + it('hides PhotoPreview on thumbnail click', () => { + + // render photo field instance + var Photo = TestUtils.renderIntoDocument( + <PhotoField /> + ); + + // click thumbnail + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + // get reference to rendered PhotoPreview instance + var PhotoPreviewInstance = TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); + + // click close button on preview + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(PhotoPreviewInstance, 'btn-photo-close') + ); + + // check that there are no longer any rendered PhotoPreview instances + var PhotoPreviewInstances = TestUtils.scryRenderedComponentsWithType(Photo, PhotoPreview); + + expect(PhotoPreviewInstances.length).toEqual(0); + }); + + it('calls onDelete when delete button pressed in preview', () => { + + var callback = jest.genMockFunction(); + + // render photo field instance + var Photo = TestUtils.renderIntoDocument( + <PhotoField buttonFunction={callback} /> + ); + + // click thumbnail + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') + ); + + // get reference to rendered PhotoPreview instance + var PhotoPreviewInstance = TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); + + // click close button on preview + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(PhotoPreviewInstance, 'btn-photo-delete') + ); + + // check that the delete method was called (which in turn calls buttonFunction prop) + expect(callback).toBeCalled(); + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoPreview-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoPreview-tests.js new file mode 100644 index 00000000..881cc5b3 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/PhotoPreview-tests.js @@ -0,0 +1,49 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('PhotoPreview', () => { + var PhotoPreview; + + beforeEach(function() { + jest.dontMock('../PhotoPreview.js'); + PhotoPreview = require('../PhotoPreview'); + }); + + it('calls onClose property when close button is pressed', () => { + var callback = jest.genMockFunction(); + + // Render a DontKnow in the document + var preview = TestUtils.renderIntoDocument( + <PhotoPreview onClose={callback} onDelete={noop} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(preview, 'btn-photo-close') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); + + it('calls onDelete property when close button is pressed', () => { + var callback = jest.genMockFunction(); + + // Render a DontKnow in the document + var preview = TestUtils.renderIntoDocument( + <PhotoPreview onClose={noop} onDelete={callback} /> + ); + + // Simulate click on button + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(preview, 'btn-photo-delete') + ); + + // Verify that the callback was called once. + expect(callback.mock.calls.length).toEqual(1); + }); +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseField-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseField-tests.js new file mode 100644 index 00000000..bb135bde --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseField-tests.js @@ -0,0 +1,451 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('ResponseField', () => { + var ResponseField, callback, setCustomValidity; + + beforeEach(function() { + jest.dontMock('../ResponseField.js'); + ResponseField = require('../ResponseField'); + callback = jest.genMockFunction(); + setCustomValidity = jest.genMockFunction(); + }); + + it('renders a number input if prop type is integer', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='integer' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('number'); + + // simulate input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 50 + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a number input if prop type is decimal', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='decimal' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('number'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 50 + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a datetime-local input if prop type is timestamp', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='timestamp' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('datetime-local'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 1238592847 + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a time input if prop type is time', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='time' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 1238592847 + } + } + ); + + expect(input.getAttribute('type')).toEqual('time'); + }); + + it('renders a date input if prop type is date', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='date' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('date'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2013-01-01T00:00:00.000Z' + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders an email input if prop type is email', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='email' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('email'); + }); + + it('renders a text input if prop type is not specified', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField type='' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + expect(input.getAttribute('type')).toEqual('text'); + + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'onetwothree' + } + } + ); + + expect(setCustomValidity).toBeCalled(); + }); + + it('renders a minus if prop showMinus is true', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField showMinus={true} buttonFunction={callback} /> + ); + + // check that minus is rendered + TestUtils.findRenderedDOMComponentWithClass(ResponseFieldInstance, 'question__minus'); + }); + + it('calls buttonFunction when minus is clicked', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField showMinus={true} buttonFunction={callback} /> + ); + + // click on minus + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass(ResponseFieldInstance, 'question__minus') + ); + + // check that the delete method was called (which in turn calls buttonFunction prop) + expect(callback).toBeCalled(); + }); + + it('calls onInput prop via onChange when input changes', () => { + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: noop, + value: 'a' + } + } + ); + + // check that the delete method was called (which in turn calls buttonFunction prop) + expect(callback).toBeCalled(); + }); + + it('validates non-numeric values in integer field', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='integer' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'a' + } + } + ); + + // called once with empty string to clear, then again with invalid message + expect(setCustomValidity.mock.calls[0][0]).toEqual(''); + expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); + }); + + it('validates min and max values in integer field', () => { + + var logic = { + min: 10, + max: 20 + }; + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='integer' logic={logic} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate correct input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 15 + } + } + ); + + // should be only called once if validation passes + expect(setCustomValidity.mock.calls.length).toEqual(1); + + // simulate bad min input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 5 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(3); + + // simulate bad max input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 25 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(5); + }); + + it('validates non-numeric values in decimal field', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='decimal' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'a' + } + } + ); + + // called once with empty string to clear, then again with invalid message + expect(setCustomValidity.mock.calls[0][0]).toEqual(''); + expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); + }); + + it('validates min and max values in decimal field', () => { + + var logic = { + min: 10, + max: 20 + }; + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='decimal' logic={logic} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate correct input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 15 + } + } + ); + + // should be only called once if validation passes + expect(setCustomValidity.mock.calls.length).toEqual(1); + + // simulate bad min input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 5 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(3); + + // simulate bad max input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 25 + } + } + ); + + // should be called two more times when validation passes + expect(setCustomValidity.mock.calls.length).toEqual(5); + }); + + it('validates non-parseable date values in date field', () => { + + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='date' /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate changing input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: 'gibberish' + } + } + ); + + // called once with empty string to clear, then again with invalid message + expect(setCustomValidity.mock.calls[0][0]).toEqual(''); + expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); + }); + + it('validates min and max values in date field', () => { + + var logic = { + min: '2014-01-01', + max: '2015-01-01' + }; + + var ResponseFieldInstance = TestUtils.renderIntoDocument( + <ResponseField onInput={callback} type='date' logic={logic} /> + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); + + // simulate correct input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2014-06-01' + } + } + ); + + // should be only called once if validation passes + expect(setCustomValidity.mock.calls.length).toEqual(1); + + // simulate bad min input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2013-01-01' + } + } + ); + + // should be called two more times when validation doesn't pass + expect(setCustomValidity.mock.calls.length).toEqual(3); + + // simulate bad max input + TestUtils.Simulate.change( + input, + { + target: { + setCustomValidity: setCustomValidity, + value: '2016-01-01' + } + } + ); + + // should be called two more times when validation doesn't pass + expect(setCustomValidity.mock.calls.length).toEqual(5); + }); + +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseFields-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseFields-tests.js new file mode 100644 index 00000000..922d19ae --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/ResponseFields-tests.js @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('ResponseFields', () => { + var ResponseFields, ResponseField; + + beforeEach(function() { + jest.dontMock('../ResponseFields.js'); + jest.dontMock('../ResponseField.js'); + ResponseFields = require('../ResponseFields'); + ResponseField = require('../ResponseField'); + }); + + it('renders multiple ResponseField components based on childCount prop', () => { + var ResponseFieldsInstance = TestUtils.renderIntoDocument( + <ResponseFields childCount='5' onInput={noop} buttonFunction={noop} type='date'/> + ); + + var ResponseFieldInstances = TestUtils.scryRenderedComponentsWithType(ResponseFieldsInstance, ResponseField); + + expect(ResponseFieldInstances.length).toEqual(5); + }); +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Select-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Select-tests.js new file mode 100644 index 00000000..5102d64b --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Select-tests.js @@ -0,0 +1,139 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Select', () => { + var Select, ResponseField, callback; + + beforeEach(function() { + jest.dontMock('../Select.js'); + jest.dontMock('../ResponseField.js'); + Select = require('../Select'); + ResponseField = require('../ResponseField'); + callback = jest.genMockFunction(); + }); + + it('renders a single select dropdown', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} /> + ); + + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + expect(select.getAttribute('multiple')).toBeNull(); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + + // expect two, as placeholder is added to top + expect(options.length).toEqual(2); + }); + + it('renders a multi select dropdown', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={true} /> + ); + + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + expect(select.getAttribute('multiple')).toBeDefined(); + }); + + it('renders other option', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} withOther={true} /> + ); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + // expect two, as placeholder is added to top + expect(options.length).toEqual(3); + }); + + it('renders other field when other option selected', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} withOther={true} /> + ); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + TestUtils.Simulate.change( + select, + { + target: { + selectedOptions: [ + options[2] + ] + } + } + ); + + // make sure the response field is rendered + var ResponseFieldInstance = TestUtils.findRenderedComponentWithType(SelectInstance, ResponseField); + }); + + it('calls onSelect prop when changed', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} onSelect={callback} /> + ); + + var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + TestUtils.Simulate.change( + select, + { + target: { + selectedOptions: [ + options[1] + ] + } + } + ); + + expect(callback).toBeCalled(); + + }); + + it('renders with default value from initSelect', () => { + var choices = [{ + value: 0, + text: 'Zero' + }]; + + var SelectInstance = TestUtils.renderIntoDocument( + <Select choices={choices} multiSelect={false} initSelect={[0]} /> + ); + + var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); + + expect(select.value).toEqual('0'); + }); +}); diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Title-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Title-tests.js new file mode 100644 index 00000000..75e0bdb9 --- /dev/null +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/Title-tests.js @@ -0,0 +1,28 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('Title', () => { + var Title; + + beforeEach(function() { + jest.dontMock('../Title.js'); + Title = require('../Title'); + }); + + it('should render a title with a message', () => { + var title = TestUtils.renderIntoDocument( + <Title title='Hello' message='Hola' /> + ); + + var titleNode = TestUtils.findRenderedDOMComponentWithTag(title, 'h3'); + var messageNode = TestUtils.findRenderedDOMComponentWithTag(title, 'p'); + + // Verify that its text matches Press Me! + expect(titleNode.textContent).toEqual('Hello'); + expect(messageNode.textContent).toEqual('Hola'); + }); +}); diff --git a/dokomoforms/static/src/survey/js/services/__tests__/auth-tests.js b/dokomoforms/static/src/survey/js/services/__tests__/auth-tests.js new file mode 100644 index 00000000..055be0af --- /dev/null +++ b/dokomoforms/static/src/survey/js/services/__tests__/auth-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('auth', () => { + var auth; + + beforeEach(function() { + jest.dontMock('../auth.js'); + auth = require('../auth'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/services/__tests__/location-tests.js b/dokomoforms/static/src/survey/js/services/__tests__/location-tests.js new file mode 100644 index 00000000..bb4685a5 --- /dev/null +++ b/dokomoforms/static/src/survey/js/services/__tests__/location-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('location', () => { + var location; + + beforeEach(function() { + jest.dontMock('../location.js'); + location = require('../location'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/dokomoforms/static/src/survey/js/services/__tests__/utils-tests.js b/dokomoforms/static/src/survey/js/services/__tests__/utils-tests.js new file mode 100644 index 00000000..adde87a4 --- /dev/null +++ b/dokomoforms/static/src/survey/js/services/__tests__/utils-tests.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +// jest.autoMockOff(); + +// a noop function useful for passing into components that require it. +var noop = () => {}; + +describe('utils', () => { + var utils; + + beforeEach(function() { + jest.dontMock('../utils.js'); + utils = require('../utils'); + }); + + it('does nothing', () => { + + }); + +}); diff --git a/package.json b/package.json index 4bee729a..edae0f75 100644 --- a/package.json +++ b/package.json @@ -92,41 +92,37 @@ "js" ], "unmockedModulePathPatterns": [ - "<rootDir>/node_modules/react", - "<rootDir>/node_modules/react-dom", - "<rootDir>/node_modules/react-addons-test-utils", - "<rootDir>/node_modules/fbjs" + "<rootDir>/node_modules/" ], - "testDirectoryName": "tests/js", "collectCoverage": true, "collectCoverageOnlyFrom": { - "dokomoforms/static/src/survey/js/Application.js": false, - "dokomoforms/static/src/survey/js/api/FacilityAPI.js": false, - "dokomoforms/static/src/survey/js/api/PhotoAPI.js": false, - "dokomoforms/static/src/common/js/persona.js": false, - "dokomoforms/static/src/survey/js/components/Facility.js": false, - "dokomoforms/static/src/survey/js/components/Footer.js": false, - "dokomoforms/static/src/survey/js/components/Header.js": false, - "dokomoforms/static/src/survey/js/components/Location.js": false, - "dokomoforms/static/src/survey/js/components/MultipleChoice.js": false, - "dokomoforms/static/src/survey/js/components/Note.js": false, - "dokomoforms/static/src/survey/js/components/Photo.js": false, - "dokomoforms/static/src/survey/js/components/Question.js": false, - "dokomoforms/static/src/survey/js/components/Splash.js": false, - "dokomoforms/static/src/survey/js/components/Submit.js": false, + "dokomoforms/static/src/survey/js/Application.js": true, + "dokomoforms/static/src/survey/js/api/FacilityAPI.js": true, + "dokomoforms/static/src/survey/js/api/PhotoAPI.js": true, + "dokomoforms/static/src/common/js/persona.js": true, + "dokomoforms/static/src/survey/js/components/Facility.js": true, + "dokomoforms/static/src/survey/js/components/Footer.js": true, + "dokomoforms/static/src/survey/js/components/Header.js": true, + "dokomoforms/static/src/survey/js/components/Location.js": true, + "dokomoforms/static/src/survey/js/components/MultipleChoice.js": true, + "dokomoforms/static/src/survey/js/components/Note.js": true, + "dokomoforms/static/src/survey/js/components/Photo.js": true, + "dokomoforms/static/src/survey/js/components/Question.js": true, + "dokomoforms/static/src/survey/js/components/Splash.js": true, + "dokomoforms/static/src/survey/js/components/Submit.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/Card.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/Message.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/Menu.js": true, - "dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js": false, - "dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js": false, + "dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js": true, "dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js": true, - "dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js": false, - "dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields.js": false, - "dokomoforms/static/src/survey/js/components/baseComponents/Select.js": false, - "dokomoforms/static/src/survey/js/components/baseComponents/Title.js": false + "dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/Select.js": true, + "dokomoforms/static/src/survey/js/components/baseComponents/Title.js": true } }, "scripts": { diff --git a/tests/js/README.md b/tests/js/README.md new file mode 100644 index 00000000..d349ed51 --- /dev/null +++ b/tests/js/README.md @@ -0,0 +1,15 @@ +# JavaScript Tests + +### Unit Tests + +The frontend javascript is set up for unit testing using the [Jest](http://facebook.github.io/jest/) testing framework. At the time of this writing, only a small portion of the javascript code base was adequately unit tested, but each React component has a stub test file which can be populated with tests. + +As per Jest's recommended best practice, tests for components are located in an **__tests__** directory which sits along side the components themselves. + +#### Running the Unit Tests + +Assuming the npm dependencies have been installed, run `$ npm test`. + +### Functional Tests + +Frontend functionality is covered using Selenium tests. These tests can be found in **/tests/python/test_selenium.py**. For more info about running these tests, see [the wiki](https://github.com/SEL-Columbia/dokomoforms/wiki/Local-Development-Environment#running-tests). diff --git a/tests/js/base-component-tests.js b/tests/js/base-component-tests.js deleted file mode 100644 index 4a5f3b98..00000000 --- a/tests/js/base-component-tests.js +++ /dev/null @@ -1,1079 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; - -// a noop function useful for passing into components that require it. -var noop = () => {}; - -describe('BigButton', () => { - var BigButton; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton.js'); - BigButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/BigButton'); - }); - - it('renders displaying its text property', () => { - // Render a BigButton in the document - var button = TestUtils.renderIntoDocument( - <BigButton text='Press Me!' buttonFunction={noop} /> - ); - - // Get the rendered element - var buttonNode = ReactDOM.findDOMNode(button); - - // Verify that its text matches Press Me! - expect(buttonNode.textContent).toEqual('Press Me!'); - }); - - it('calls the buttonFunction property when pressed', () => { - var callback = jest.genMockFunction(); - - // Render a BigButton in the document - var button = TestUtils.renderIntoDocument( - <BigButton text='Press Me!' buttonFunction={callback} /> - ); - - // Simulate click on button - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithTag(button, 'button') - ); - - // Verify that the callback was called once. - expect(callback.mock.calls.length).toEqual(1); - }); - - it('should add a class associated with a type property', () => { - // Render a BigButton in the document - var button = TestUtils.renderIntoDocument( - <BigButton text='Press Me!' buttonFunction={noop} type='btn-testing' /> - ); - - // makes sure the type property has added the class - TestUtils.findRenderedDOMComponentWithClass(button, 'btn-testing'); - }); - -}); - -describe('LittleButton', () => { - var LittleButton; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton.js'); - LittleButton = require('../../dokomoforms/static/src/survey/js/components/baseComponents/LittleButton'); - }); - - it('renders displaying its text property', () => { - // Render a LittleButton in the document - var button = TestUtils.renderIntoDocument( - <LittleButton text='Press Me!' buttonFunction={noop} /> - ); - - // Get the rendered element - var buttonNode = ReactDOM.findDOMNode(button); - - // Verify that its text matches Press Me! - expect(buttonNode.textContent).toEqual('Press Me!'); - }); - - it('calls the buttonFunction property when pressed', () => { - var callback = jest.genMockFunction(); - - // Render a LittleButton in the document - var button = TestUtils.renderIntoDocument( - <LittleButton text='Press Me!' buttonFunction={callback} /> - ); - - // Simulate click on button - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithTag(button, 'button') - ); - - // Verify that the callback was called once. - expect(callback.mock.calls.length).toEqual(1); - }); - - it('is disabled when disabled property is set', () => { - var callback = jest.genMockFunction(); - - // Render a LittleButton in the document - var button = TestUtils.renderIntoDocument( - <LittleButton text='Press Me!' buttonFunction={callback} disabled={true} /> - ); - - // Simulate click on button - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithTag(button, 'button') - ); - - // Verify that the callback was called once. - expect(callback.mock.calls.length).toEqual(0); - }); - - it('includes an icon if an icon property is set', () => { - // Render a LittleButton in the document - var button = TestUtils.renderIntoDocument( - <LittleButton text='Press Me!' buttonFunction={noop} icon="icon-test" /> - ); - - // makes sure there is a span tag (i.e. icon) rendered within the button - TestUtils.findRenderedDOMComponentWithTag(button, 'span'); - }); - -}); - -describe('Card', () => { - var Card; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Card.js'); - Card = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Card'); - }); - - it('should render a sinlge message', () => { - var message = ['hello']; - - var card = TestUtils.renderIntoDocument( - <Card messages={message} /> - ); - - // makes sure there is a single span tag (i.e. message) - TestUtils.findRenderedDOMComponentWithTag(card, 'span'); - }); - - it('should render multiple messages', () => { - var messages = ['hello', 'hi', 'hola']; - - var card = TestUtils.renderIntoDocument( - <Card messages={messages} /> - ); - - // makes sure there is a single span tag (i.e. message) - var spans = TestUtils.scryRenderedDOMComponentsWithTag(card, 'span'); - - expect(spans.length).toEqual(3); - }); - - it('should add a class associated with a type property', () => { - var message = ['hello']; - - var card = TestUtils.renderIntoDocument( - <Card messages={message} type='message-secondary' /> - ); - - // makes sure there the type prop has added the class - TestUtils.findRenderedDOMComponentWithClass(card, 'message-secondary'); - }); -}); - -describe('Message', () => { - var Message; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Message.js'); - Message = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Message'); - }); - - it('should render a sinlge message', () => { - var message = TestUtils.renderIntoDocument( - <Message text='Hello' /> - ); - - // Get the rendered element - var messageNode = ReactDOM.findDOMNode(message); - - // Verify that its text matches Press Me! - expect(messageNode.textContent).toEqual('Hello'); - }); - - - it('should add classes associated with the classes property', () => { - var classes = 'testing'; - - var message = TestUtils.renderIntoDocument( - <Message text='Hello' classes={classes} /> - ); - - // makes sure there the type prop has added the class - TestUtils.findRenderedDOMComponentWithClass(message, 'testing'); - }); -}); - -describe('Title', () => { - var Title; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Title.js'); - Title = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Title'); - }); - - it('should render a title with a message', () => { - var title = TestUtils.renderIntoDocument( - <Title title='Hello' message='Hola' /> - ); - - var titleNode = TestUtils.findRenderedDOMComponentWithTag(title, 'h3'); - var messageNode = TestUtils.findRenderedDOMComponentWithTag(title, 'p'); - - // Verify that its text matches Press Me! - expect(titleNode.textContent).toEqual('Hello'); - expect(messageNode.textContent).toEqual('Hola'); - }); -}); - - -describe('DontKnow', () => { - var DontKnow; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow.js'); - DontKnow = require('../../dokomoforms/static/src/survey/js/components/baseComponents/DontKnow'); - }); - - it('calls the checkBoxFunction property when pressed', () => { - var callback = jest.genMockFunction(); - - // Render a DontKnow in the document - var dontKnow = TestUtils.renderIntoDocument( - <DontKnow checkBoxFunction={callback} /> - ); - - // Simulate click on button - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input') - ); - - // Verify that the callback was called once. - expect(callback.mock.calls.length).toEqual(1); - }); - - it('is not checked by default', () => { - // Render a DontKnow in the document - var dontKnow = TestUtils.renderIntoDocument( - <DontKnow checkBoxFunction={noop} /> - ); - - // Get the rendered element - var dontKnowNode = TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input'); - - expect(dontKnowNode.defaultChecked).toEqual(false); - }); - - it('is checked by default when checked property is present', () => { - // Render a DontKnow in the document - var dontKnow = TestUtils.renderIntoDocument( - <DontKnow checkBoxFunction={noop} checked={true} /> - ); - - // Get the rendered element - var dontKnowNode = TestUtils.findRenderedDOMComponentWithTag(dontKnow, 'input'); - - expect(dontKnowNode.defaultChecked).toEqual(true); - }); -}); - -describe('PhotoPreview', () => { - var PhotoPreview; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); - PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); - }); - - it('calls onClose property when close button is pressed', () => { - var callback = jest.genMockFunction(); - - // Render a DontKnow in the document - var preview = TestUtils.renderIntoDocument( - <PhotoPreview onClose={callback} onDelete={noop} /> - ); - - // Simulate click on button - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(preview, 'btn-photo-close') - ); - - // Verify that the callback was called once. - expect(callback.mock.calls.length).toEqual(1); - }); - - it('calls onDelete property when close button is pressed', () => { - var callback = jest.genMockFunction(); - - // Render a DontKnow in the document - var preview = TestUtils.renderIntoDocument( - <PhotoPreview onClose={noop} onDelete={callback} /> - ); - - // Simulate click on button - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(preview, 'btn-photo-delete') - ); - - // Verify that the callback was called once. - expect(callback.mock.calls.length).toEqual(1); - }); -}); - -describe('PhotoField', () => { - var PhotoField; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField.js'); - PhotoField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoField'); - }); - - it('calls showPreview on thumbnail click', () => { - - // Hackity hack hack hack - // https://github.com/facebook/jest/issues/207 - PhotoField.prototype.__reactAutoBindMap.showPreview = jest.genMockFunction(); - - var Photo = TestUtils.renderIntoDocument( - <PhotoField /> - ); - - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') - ); - - expect(PhotoField.prototype.__reactAutoBindMap.showPreview).toBeCalled(); - }); - - it('renders image tag when initValue passed', () => { - - var Photo = TestUtils.renderIntoDocument( - <PhotoField initValue='nothing.jpg' /> - ); - - TestUtils.findRenderedDOMComponentWithTag(Photo, 'img'); - - }); - - it('renders PhotoPreview on thumbnail click', () => { - - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); - var PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); - - var Photo = TestUtils.renderIntoDocument( - <PhotoField /> - ); - - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') - ); - - TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); - }); - - - - it('hides PhotoPreview on thumbnail click', () => { - - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); - var PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); - - // render photo field instance - var Photo = TestUtils.renderIntoDocument( - <PhotoField /> - ); - - // click thumbnail - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') - ); - - // get reference to rendered PhotoPreview instance - var PhotoPreviewInstance = TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); - - // click close button on preview - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(PhotoPreviewInstance, 'btn-photo-close') - ); - - // check that there are no longer any rendered PhotoPreview instances - var PhotoPreviewInstances = TestUtils.scryRenderedComponentsWithType(Photo, PhotoPreview); - - expect(PhotoPreviewInstances.length).toEqual(0); - }); - - it('calls onDelete when delete button pressed in preview', () => { - - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview.js'); - var PhotoPreview = require('../../dokomoforms/static/src/survey/js/components/baseComponents/PhotoPreview'); - - var callback = jest.genMockFunction(); - - // render photo field instance - var Photo = TestUtils.renderIntoDocument( - <PhotoField buttonFunction={callback} /> - ); - - // click thumbnail - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(Photo, 'photo_container') - ); - - // get reference to rendered PhotoPreview instance - var PhotoPreviewInstance = TestUtils.findRenderedComponentWithType(Photo, PhotoPreview); - - // click close button on preview - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(PhotoPreviewInstance, 'btn-photo-delete') - ); - - // check that the delete method was called (which in turn calls buttonFunction prop) - expect(callback).toBeCalled(); - }); - -}); - -describe('ResponseField', () => { - var ResponseField, callback, setCustomValidity; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js'); - ResponseField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField'); - callback = jest.genMockFunction(); - setCustomValidity = jest.genMockFunction(); - }); - - it('renders a number input if prop type is integer', () => { - - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='integer' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - expect(input.getAttribute('type')).toEqual('number'); - - // simulate input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 50 - } - } - ); - - expect(setCustomValidity).toBeCalled(); - }); - - it('renders a number input if prop type is decimal', () => { - - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='decimal' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - expect(input.getAttribute('type')).toEqual('number'); - - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 50 - } - } - ); - - expect(setCustomValidity).toBeCalled(); - }); - - it('renders a datetime-local input if prop type is timestamp', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='timestamp' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - expect(input.getAttribute('type')).toEqual('datetime-local'); - - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 1238592847 - } - } - ); - - expect(setCustomValidity).toBeCalled(); - }); - - it('renders a time input if prop type is time', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='time' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 1238592847 - } - } - ); - - expect(input.getAttribute('type')).toEqual('time'); - }); - - it('renders a date input if prop type is date', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='date' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - expect(input.getAttribute('type')).toEqual('date'); - - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: '2013-01-01T00:00:00.000Z' - } - } - ); - - expect(setCustomValidity).toBeCalled(); - }); - - it('renders an email input if prop type is email', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='email' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - expect(input.getAttribute('type')).toEqual('email'); - }); - - it('renders a text input if prop type is not specified', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField type='' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - expect(input.getAttribute('type')).toEqual('text'); - - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 'onetwothree' - } - } - ); - - expect(setCustomValidity).toBeCalled(); - }); - - it('renders a minus if prop showMinus is true', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField showMinus={true} buttonFunction={callback} /> - ); - - // check that minus is rendered - TestUtils.findRenderedDOMComponentWithClass(ResponseFieldInstance, 'question__minus'); - }); - - it('calls buttonFunction when minus is clicked', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField showMinus={true} buttonFunction={callback} /> - ); - - // click on minus - TestUtils.Simulate.click( - TestUtils.findRenderedDOMComponentWithClass(ResponseFieldInstance, 'question__minus') - ); - - // check that the delete method was called (which in turn calls buttonFunction prop) - expect(callback).toBeCalled(); - }); - - it('calls onInput prop via onChange when input changes', () => { - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate changing input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: noop, - value: 'a' - } - } - ); - - // check that the delete method was called (which in turn calls buttonFunction prop) - expect(callback).toBeCalled(); - }); - - it('validates non-numeric values in integer field', () => { - - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='integer' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate changing input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 'a' - } - } - ); - - // called once with empty string to clear, then again with invalid message - expect(setCustomValidity.mock.calls[0][0]).toEqual(''); - expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); - }); - - it('validates min and max values in integer field', () => { - - var logic = { - min: 10, - max: 20 - }; - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='integer' logic={logic} /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate correct input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 15 - } - } - ); - - // should be only called once if validation passes - expect(setCustomValidity.mock.calls.length).toEqual(1); - - // simulate bad min input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 5 - } - } - ); - - // should be called two more times when validation passes - expect(setCustomValidity.mock.calls.length).toEqual(3); - - // simulate bad max input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 25 - } - } - ); - - // should be called two more times when validation passes - expect(setCustomValidity.mock.calls.length).toEqual(5); - }); - - it('validates non-numeric values in decimal field', () => { - - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='decimal' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate changing input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 'a' - } - } - ); - - // called once with empty string to clear, then again with invalid message - expect(setCustomValidity.mock.calls[0][0]).toEqual(''); - expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); - }); - - it('validates min and max values in decimal field', () => { - - var logic = { - min: 10, - max: 20 - }; - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='decimal' logic={logic} /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate correct input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 15 - } - } - ); - - // should be only called once if validation passes - expect(setCustomValidity.mock.calls.length).toEqual(1); - - // simulate bad min input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 5 - } - } - ); - - // should be called two more times when validation passes - expect(setCustomValidity.mock.calls.length).toEqual(3); - - // simulate bad max input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 25 - } - } - ); - - // should be called two more times when validation passes - expect(setCustomValidity.mock.calls.length).toEqual(5); - }); - - it('validates non-parseable date values in date field', () => { - - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='date' /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate changing input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: 'gibberish' - } - } - ); - - // called once with empty string to clear, then again with invalid message - expect(setCustomValidity.mock.calls[0][0]).toEqual(''); - expect(setCustomValidity.mock.calls[1][0]).toEqual('Invalid field.'); - }); - - it('validates min and max values in date field', () => { - - var logic = { - min: '2014-01-01', - max: '2015-01-01' - }; - - var ResponseFieldInstance = TestUtils.renderIntoDocument( - <ResponseField onInput={callback} type='date' logic={logic} /> - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(ResponseFieldInstance, 'input'); - - // simulate correct input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: '2014-06-01' - } - } - ); - - // should be only called once if validation passes - expect(setCustomValidity.mock.calls.length).toEqual(1); - - // simulate bad min input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: '2013-01-01' - } - } - ); - - // should be called two more times when validation doesn't pass - expect(setCustomValidity.mock.calls.length).toEqual(3); - - // simulate bad max input - TestUtils.Simulate.change( - input, - { - target: { - setCustomValidity: setCustomValidity, - value: '2016-01-01' - } - } - ); - - // should be called two more times when validation doesn't pass - expect(setCustomValidity.mock.calls.length).toEqual(5); - }); - -}); - -describe('ResponseFields', () => { - var ResponseFields, ResponseField; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields.js'); - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js'); - ResponseFields = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseFields'); - ResponseField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField'); - }); - - it('renders multiple ResponseField components based on childCount prop', () => { - var ResponseFieldsInstance = TestUtils.renderIntoDocument( - <ResponseFields childCount='5' onInput={noop} buttonFunction={noop} type='date'/> - ); - - var ResponseFieldInstances = TestUtils.scryRenderedComponentsWithType(ResponseFieldsInstance, ResponseField); - - expect(ResponseFieldInstances.length).toEqual(5); - }); -}); - -describe('Select', () => { - var Select, ResponseField, callback; - - beforeEach(function() { - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/Select.js'); - jest.dontMock('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField.js'); - Select = require('../../dokomoforms/static/src/survey/js/components/baseComponents/Select'); - ResponseField = require('../../dokomoforms/static/src/survey/js/components/baseComponents/ResponseField'); - callback = jest.genMockFunction(); - }); - - it('renders a single select dropdown', () => { - var choices = [{ - value: 0, - text: 'Zero' - }]; - - var SelectInstance = TestUtils.renderIntoDocument( - <Select choices={choices} multiSelect={false} /> - ); - - var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); - - expect(select.getAttribute('multiple')).toBeNull(); - - var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); - - // expect two, as placeholder is added to top - expect(options.length).toEqual(2); - }); - - it('renders a multi select dropdown', () => { - var choices = [{ - value: 0, - text: 'Zero' - }]; - - var SelectInstance = TestUtils.renderIntoDocument( - <Select choices={choices} multiSelect={true} /> - ); - - var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); - - expect(select.getAttribute('multiple')).toBeDefined(); - }); - - it('renders other option', () => { - var choices = [{ - value: 0, - text: 'Zero' - }]; - - var SelectInstance = TestUtils.renderIntoDocument( - <Select choices={choices} multiSelect={false} withOther={true} /> - ); - - var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); - // expect two, as placeholder is added to top - expect(options.length).toEqual(3); - }); - - it('renders other field when other option selected', () => { - var choices = [{ - value: 0, - text: 'Zero' - }]; - - var SelectInstance = TestUtils.renderIntoDocument( - <Select choices={choices} multiSelect={false} withOther={true} /> - ); - - var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); - var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); - - TestUtils.Simulate.change( - select, - { - target: { - selectedOptions: [ - options[2] - ] - } - } - ); - - // make sure the response field is rendered - var ResponseFieldInstance = TestUtils.findRenderedComponentWithType(SelectInstance, ResponseField); - }); - - it('calls onSelect prop when changed', () => { - var choices = [{ - value: 0, - text: 'Zero' - }]; - - var SelectInstance = TestUtils.renderIntoDocument( - <Select choices={choices} multiSelect={false} onSelect={callback} /> - ); - - var options = TestUtils.scryRenderedDOMComponentsWithTag(SelectInstance, 'option'); - var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); - - TestUtils.Simulate.change( - select, - { - target: { - selectedOptions: [ - options[1] - ] - } - } - ); - - expect(callback).toBeCalled(); - - }); - - it('renders with default value from initSelect', () => { - var choices = [{ - value: 0, - text: 'Zero' - }]; - - var SelectInstance = TestUtils.renderIntoDocument( - <Select choices={choices} multiSelect={false} initSelect={[0]} /> - ); - - var select = TestUtils.findRenderedDOMComponentWithTag(SelectInstance, 'select'); - - expect(select.value).toEqual('0'); - }); -}); - -// describe('Menu', () => { -// var survey = { -// languages: ['English', 'French'] -// }, -// surveyId = 0, -// db; -// beforeEach(function() { -// // Set up some mocked out file info before each test -// db = require('pouchdb'); -// }); - -// it('renders log out/in and revisit reload only when online', () => { -// navigator.onLine = false; - -// var menu = TestUtils.renderIntoDocument( -// <Menu -// language='English' -// survey={survey} -// surveyID={surveyId} -// db={db} -// loggedIn={false} -// hasFacilities={false} -// /> -// ); - -// var loginBtn = TestUtils.scryRenderedDOMComponentsWithClass(menu, 'menu_login'); -// var logoutBtn = TestUtils.scryRenderedDOMComponentsWithClass(menu, 'menu_logout'); - -// expect(loginBtn.length).toEqual(0); -// expect(logoutBtn.length).toEqual(0); - -// }); - -// it('renders log out when user is logged in', () => { - -// }); - -// it('renders log in when user is not logged in', () => { - -// }); - -// it('calls logout function when logout is pressed', () => { - -// }); - -// it('calls login function when login is pressed', () => { - -// }); - -// }); From 89f5f3a28e16989708b23ecf43d3a4887f6df7a1 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Tue, 24 Nov 2015 16:57:23 -0500 Subject: [PATCH 23/69] added tests for FacilityRadios --- .../baseComponents/FacilityRadios.js | 5 +- .../__tests__/FacilityRadios-tests.js | 178 +++++++++++++++++- 2 files changed, 177 insertions(+), 6 deletions(-) diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js b/dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js index 17f667e9..5bf3adb3 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/FacilityRadios.js @@ -38,11 +38,10 @@ module.exports = React.createClass({ e.target.checked = checked; window.etarget = e.target; - //e.stopPropagation(); - //e.cancelBubble = true; - if (this.props.selectFunction) + if (this.props.selectFunction) { this.props.selectFunction(selected); + } this.setState({ selected: selected diff --git a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js index 72e5089c..257911cc 100644 --- a/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js +++ b/dokomoforms/static/src/survey/js/components/baseComponents/__tests__/FacilityRadios-tests.js @@ -3,18 +3,190 @@ import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; // a noop function useful for passing into components that require it. -var noop = () => {}; +var noop = () => {}, + facilities = [{ + 'name': 'water point', + 'coordinates': [6.456968, 12.91716], + '_version': 0, + 'properties': { + 'sector': 'water', + 'type': 'type unavailable', + 'grid_power': null, + 'improved_water_supply': null, + 'improved_sanitation': null, + 'visits': 0, + 'photoUrls': [] + }, + 'identifiers': [], + 'updatedAt': '2014-12-04T21:54:37.177Z', + 'createdAt': '2014-12-04T21:54:37.177Z', + 'active': true, + 'href': 'http://staging.revisit.global/api/v0/facilities/5480d81d2ecfcf69084425f3.json', + 'uuid': '5480d81d2ecfcf69084425f3' + }, { + 'name': 'water point', + 'coordinates': [6.459725, 12.91686], + '_version': 0, + 'properties': { + 'sector': 'water', + 'type': 'type unavailable', + 'grid_power': null, + 'improved_water_supply': null, + 'improved_sanitation': null, + 'visits': 0, + 'photoUrls': [] + }, + 'identifiers': [], + 'updatedAt': '2014-12-04T21:54:37.177Z', + 'createdAt': '2014-12-04T21:54:37.177Z', + 'active': true, + 'href': 'http://staging.revisit.global/api/v0/facilities/5480d81d2ecfcf69084425b0.json', + 'uuid': '5480d81d2ecfcf69084425b0', + 'distance': 100 + }, { + 'name': 'water point', + 'coordinates': [6.454339, 12.92059], + '_version': 0, + 'properties': { + 'sector': 'water', + 'type': 'type unavailable', + 'grid_power': null, + 'improved_water_supply': null, + 'improved_sanitation': null, + 'visits': 0, + 'photoUrls': [] + }, + 'identifiers': [], + 'updatedAt': '2014-12-04T21:54:37.177Z', + 'createdAt': '2014-12-04T21:54:37.177Z', + 'active': true, + 'href': 'http://staging.revisit.global/api/v0/facilities/5480d81d2ecfcf69084425ff.json', + 'uuid': '5480d81d2ecfcf69084425ff' + }, { + 'name': 'water point', + 'coordinates': [6.456571, 12.92105], + '_version': 0, + 'properties': { + 'sector': 'water', + 'type': 'type unavailable', + 'grid_power': null, + 'improved_water_supply': null, + 'improved_sanitation': null, + 'visits': 0, + 'photoUrls': [] + }, + 'identifiers': [], + 'updatedAt': '2014-12-04T21:54:37.177Z', + 'createdAt': '2014-12-04T21:54:37.177Z', + 'active': true, + 'href': 'http://staging.revisit.global/api/v0/facilities/5480d81d2ecfcf690844261a.json', + 'uuid': '5480d81d2ecfcf690844261a' + }, { + 'name': 'water point', + 'coordinates': [6.458883, 12.92059], + '_version': 0, + 'properties': { + 'sector': 'water', + 'type': 'type unavailable', + 'grid_power': null, + 'improved_water_supply': null, + 'improved_sanitation': null, + 'visits': 0, + 'photoUrls': [] + }, + 'identifiers': [], + 'updatedAt': '2014-12-04T21:54:37.177Z', + 'createdAt': '2014-12-04T21:54:37.177Z', + 'active': true, + 'href': 'http://staging.revisit.global/api/v0/facilities/5480d81d2ecfcf6908442625.json', + 'uuid': '5480d81d2ecfcf6908442625' + }, { + 'name': 'water point', + 'coordinates': [6.459097, 12.92074], + '_version': 0, + 'properties': { + 'sector': 'water', + 'type': 'type unavailable', + 'grid_power': null, + 'improved_water_supply': null, + 'improved_sanitation': null, + 'visits': 0, + 'photoUrls': [] + }, + 'identifiers': [], + 'updatedAt': '2014-12-04T21:54:37.177Z', + 'createdAt': '2014-12-04T21:54:37.177Z', + 'active': true, + 'href': 'http://staging.revisit.global/api/v0/facilities/5480d81d2ecfcf6908442607.json', + 'uuid': '5480d81d2ecfcf6908442607' + }]; describe('FacilityRadios', () => { - var FacilityRadios; + var FacilityRadios, callback; beforeEach(function() { jest.dontMock('../FacilityRadios.js'); FacilityRadios = require('../FacilityRadios'); + callback = jest.genMockFunction(); }); - it('does nothing', () => { + it('renders no facilities message if no facilities passed', () => { + var FacilityRadiosInstance = TestUtils.renderIntoDocument( + <FacilityRadios facilities={[]} /> + ); + var message = TestUtils.findRenderedDOMComponentWithClass(FacilityRadiosInstance, 'content-padded'); + + expect(message.textContent).toEqual('No nearby facilities located.'); + }); + + it('renders facilities list', () => { + var len = facilities.length; + + var FacilityRadiosInstance = TestUtils.renderIntoDocument( + <FacilityRadios facilities={facilities} /> + ); + + var facs = TestUtils.scryRenderedDOMComponentsWithClass(FacilityRadiosInstance, 'question__radio'); + + expect(facs.length).toEqual(len); + }); + + it('calls selectFunction prop on facility click', () => { + var FacilityRadiosInstance = TestUtils.renderIntoDocument( + <FacilityRadios facilities={facilities} selectFunction={callback} /> + ); + + var facs = TestUtils.scryRenderedDOMComponentsWithTag(FacilityRadiosInstance, 'input'); + + + TestUtils.Simulate.click(facs[0]); + + expect(callback).toBeCalled(); + }); + + it('selects a facility when facility radio clicked', () => { + var FacilityRadiosInstance = TestUtils.renderIntoDocument( + <FacilityRadios facilities={facilities} selectFunction={callback} /> + ); + + var facs = TestUtils.scryRenderedDOMComponentsWithTag(FacilityRadiosInstance, 'input'); + + expect(facs[0].getAttribute('checked')).toBeNull(); + + TestUtils.Simulate.click(facs[0], { + target: facs[0] + }); + + facs = TestUtils.scryRenderedDOMComponentsWithTag(FacilityRadiosInstance, 'input'); + + expect(facs[0].getAttribute('checked')).toBeDefined(); + + TestUtils.Simulate.click(facs[0], { + target: facs[0] + }); + + expect(facs[0].getAttribute('checked')).toBeNull(); }); }); From 2e95acf8d564276f3c2e323f394d53209fc585d7 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Thu, 12 Nov 2015 15:36:12 -0500 Subject: [PATCH 24/69] added es5-shim --- gulpfile.js | 2 ++ package.json | 1 + 2 files changed, 3 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index 576d4326..9f38491e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -53,6 +53,8 @@ var path = { // SURVEY_CSS_BUILD: survey_dist_path + '/css/survey/*.css', SURVEY_JS_VENDOR_SRC: [ + node_modules_path + '/es5-shim/es5-shim.js', + node_modules_path + '/es5-shim/es5-sham.js', node_modules_path + '/jquery/dist/jquery.js', node_modules_path + '/bootstrap/dist/js/bootstrap.js', node_modules_path + '/lodash-compat/index.js', diff --git a/package.json b/package.json index edae0f75..85d162e8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "backbone": "^1.2.3", "bootstrap": "^3.3.5", "datatables": "https://github.com/DataTables/DataTables/archive/1.10.9.tar.gz", + "es5-shim": "^4.3.1", "highcharts-release": "^4.1.8", "jquery": "^2.1.4", "leaflet": "^0.7.5", From f7b9d6bd6565868d1af88a51b427757586533290 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 16 Nov 2015 12:25:57 -0500 Subject: [PATCH 25/69] This seems to help --- tests/python/test_selenium.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 65eba54c..3748eaba 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -208,6 +208,8 @@ def setUp(self): time.sleep(10) if self.browser != 'android': self.drv.set_page_load_timeout(180) + if self.browser not in {'android', 'iPhone'}: + self.drv.set_window_size(1280, 1280) self.drv.set_script_timeout(180) def _set_sauce_status(self): From d52f1014c778e8c5d004b888df0ab45d4c6808bc Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 16 Nov 2015 12:50:50 -0500 Subject: [PATCH 26/69] Skip admin-interface tests on mobile --- tests/python/test_selenium.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 3748eaba..93030466 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -373,6 +373,8 @@ def test_login(self): class AdminTest(DriverTest): def setUp(self): super().setUp() + if self.browser in {'android', 'iPhone'}: + self.skipTest('The admin interface has no mobile design (yet).') self.get('/debug/login/test_creator@fixtures.com') self.wait_for_element('html', by=By.TAG_NAME) From 1e00ad1ba8d2296be5922407982f77010a68c34c Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 16 Nov 2015 14:10:55 -0500 Subject: [PATCH 27/69] Fix a number of Android (5.0) tests --- tests/python/test_selenium.py | 286 ++++++---------------------------- 1 file changed, 45 insertions(+), 241 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 93030466..3cd77a3e 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -24,13 +24,14 @@ from selenium import webdriver from selenium.common.exceptions import ( - TimeoutException, ElementNotVisibleException + TimeoutException, ElementNotVisibleException, WebDriverException ) from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.select import Select import tests.python.util from tests.python.util import setUpModule, tearDownModule @@ -302,6 +303,9 @@ def toggle_online(self, browser=True, revisit=True): urlopen('http://localhost:9999/debug/toggle_facilities') self.sleep(1) + def input_field(self): + return self.drv.find_element_by_tag_name('input') + @property def control_key(self): is_osx = self.platform.startswith('OS X') @@ -1038,7 +1042,8 @@ def test_change_language(self): self.click(self.drv.find_element_by_class_name('nav-settings')) self.wait_for_element('user-preferred-lang') self.click(self.drv.find_element_by_css_selector( - '#user-preferred-lang option:nth-of-type(2)')) + '#user-preferred-lang option:nth-of-type(2)' + )) save_btn = self.drv.find_element_by_class_name('btn-save-user') self.sleep() @@ -1276,17 +1281,18 @@ def test_change_language(self): self.wait_for_element('menu', By.CLASS_NAME) self.click(self.drv.find_element_by_class_name('menu')) - self.click( - self.drv - .find_element_by_class_name('language_select') - ) - - self.assertEqual( - len(self.drv.find_elements_by_css_selector( - '.language_select option')), 3) - - self.click(self.drv.find_elements_by_css_selector( - '.language_select option')[1]) + lang = Select(self.drv.find_element_by_class_name('language_select')) + self.assertEqual(len(lang.options), 3) + # For some reason on Android selecting an option works but raises an + # exception... + # So... ignore the exception! + try: + lang.select_by_index(1) + except WebDriverException: + if self.browser == 'android': + pass + else: + raise self.sleep() @@ -2177,17 +2183,7 @@ def test_required_question_bad_answer(self): alert = self.drv.switch_to.alert alert.accept() - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('3') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 14, '3') self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) @@ -2254,28 +2250,14 @@ def test_allow_multiple_cant_fool_required(self): # TODO: change this behavior alert = self.drv.switch_to.alert alert.accept() + if self.browser == 'android': + self.sleep() - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_elements_by_tag_name('input')[0] - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('3') - .perform() + self.drv.find_elements_by_tag_name('input')[0].send_keys( + Keys.BACK_SPACE * 14, '3' ) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_elements_by_tag_name('input')[-1] - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('4') - .perform() + self.drv.find_elements_by_tag_name('input')[-1].send_keys( + Keys.BACK_SPACE * 13, '4' ) self.click(self.drv.find_element_by_class_name('navigate-right')) @@ -2776,16 +2758,8 @@ def test_branch_nesting(self): 'integer_0' ) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('15') - .perform() + self.drv.find_element_by_tag_name('input').send_keys( + Keys.BACK_SPACE * 2, '15' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3021,17 +2995,7 @@ def test_multiple_buckets_for_same_branch(self): 'branch' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('25') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '25') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3103,34 +3067,14 @@ def test_integer_buckets(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('4') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '4') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('1') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '1') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3155,34 +3099,14 @@ def test_integer_buckets_open_ranges(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('15') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 2, '15') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('5') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 2, '5') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3207,17 +3131,7 @@ def test_integer_buckets_total_open(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('999') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 4, '999') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3246,34 +3160,14 @@ def test_decimal_buckets(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('4.2') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 3, '4.2') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('1.2') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 3, '1.2') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3298,34 +3192,14 @@ def test_decimal_buckets_open_ranges(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('15.1') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 4, '15.1') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('5.1') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 4, '5.1') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3350,17 +3224,7 @@ def test_decimal_buckets_total_open(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('999.1') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 6, '999.1') self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, @@ -3929,17 +3793,7 @@ def test_logic_integer_min_max(self): 1 ) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('2') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '2') self.assertEqual( len(self.drv.find_elements_by_css_selector('input:invalid')), @@ -3950,17 +3804,7 @@ def test_logic_integer_min_max(self): 0 ) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('12') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '12') self.assertEqual( len(self.drv.find_elements_by_css_selector('input:invalid')), @@ -4021,17 +3865,7 @@ def test_logic_decimal_min_max(self): 1 ) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('2') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '2') self.assertEqual( len(self.drv.find_elements_by_css_selector('input:invalid')), @@ -4042,17 +3876,7 @@ def test_logic_decimal_min_max(self): 0 ) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys('12') - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE, '12') self.assertEqual( len(self.drv.find_elements_by_css_selector('input:invalid')), @@ -4120,17 +3944,7 @@ def test_logic_date_min_max(self): .send_keys(Keys.LEFT, Keys.LEFT) ) else: - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2015', '09', '02' @@ -4152,17 +3966,7 @@ def test_logic_date_min_max(self): .send_keys(Keys.LEFT, Keys.LEFT) ) else: - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2015', '09', '12' From b36d60e0202793681a161323e3c6ae6db88a06e2 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Wed, 18 Nov 2015 11:45:11 -0500 Subject: [PATCH 28/69] Fix test_facilities_only_fetched_on_first_load for Android 5.0 --- tests/python/test_selenium.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 3cd77a3e..8ee25b58 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -4367,20 +4367,25 @@ def test_facilities_only_fetched_on_first_load(self): """ survey_id = self.get_single_node_survey_id('facility') - # first load, should be slow because revisit is hit - start_time = time.time() self.get('/enumerate/{}'.format(survey_id)) - overlay = self.drv.find_elements_by_class_name('loading-overlay') - finish_time = time.time() + if self.browser == 'android': + overlay = self.drv.find_elements_by_class_name('loading-overlay') + self.assertEqual(len(overlay), 1) + self.sleep(2) + noverlay = self.drv.find_elements_by_class_name('loading-overlay') + self.assertEqual(len(noverlay), 0) - self.assertGreater(finish_time - start_time, 2) - # overlay should not be present - self.assertEqual(len(overlay), 0) + else: + # first load, should be slow because revisit is hit + start_time = time.time() + overlay = self.drv.find_elements_by_class_name('loading-overlay') + finish_time = time.time() + + self.assertGreater(finish_time - start_time, 2) + # overlay should not be present + self.assertEqual(len(overlay), 0) # second load, should be fast because revisit is not hit - start_time = time.time() self.drv.refresh() - overlay = self.drv.find_elements_by_class_name('loading-overlay') - finish_time = time.time() - - self.assertLess(finish_time - start_time, 2) + nnoverlay = self.drv.find_elements_by_class_name('loading-overlay') + self.assertEqual(len(nnoverlay), 0) From 2b6de3b7ed9bb89f89e68b6d7ce2e7a9228397c7 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Wed, 18 Nov 2015 11:52:51 -0500 Subject: [PATCH 29/69] Fix test_single_facility_question_loading for Android 5.0 --- tests/python/test_selenium.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 8ee25b58..46472e3d 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -4352,13 +4352,20 @@ def test_single_facility_question_loading(self): start_time = time.time() self.get('/enumerate/{}'.format(survey_id)) - overlay = self.drv.find_elements_by_class_name('loading-overlay') - finish_time = time.time() - - self.assertGreater(finish_time - start_time, 2) + if self.browser == 'android': + overlay = self.drv.find_elements_by_class_name('loading-overlay') + self.assertEqual(len(overlay), 1) + self.sleep(2) + noverlay = self.drv.find_elements_by_class_name('loading-overlay') + self.assertEqual(len(noverlay), 0) + else: + # first load, should be slow because revisit is hit + overlay = self.drv.find_elements_by_class_name('loading-overlay') + finish_time = time.time() - # overlay should not be present - self.assertEqual(len(overlay), 0) + self.assertGreater(finish_time - start_time, 2) + # overlay should not be present + self.assertEqual(len(overlay), 0) @report_success_status def test_facilities_only_fetched_on_first_load(self): @@ -4367,6 +4374,7 @@ def test_facilities_only_fetched_on_first_load(self): """ survey_id = self.get_single_node_survey_id('facility') + start_time = time.time() self.get('/enumerate/{}'.format(survey_id)) if self.browser == 'android': overlay = self.drv.find_elements_by_class_name('loading-overlay') @@ -4374,10 +4382,8 @@ def test_facilities_only_fetched_on_first_load(self): self.sleep(2) noverlay = self.drv.find_elements_by_class_name('loading-overlay') self.assertEqual(len(noverlay), 0) - else: # first load, should be slow because revisit is hit - start_time = time.time() overlay = self.drv.find_elements_by_class_name('loading-overlay') finish_time = time.time() From cf35d32da32c1e9f59574cc2acf81c32f8acaba5 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 23 Nov 2015 10:16:47 -0500 Subject: [PATCH 30/69] Fix broken tests --- tests/python/test_selenium.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 46472e3d..d882066d 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -2183,7 +2183,9 @@ def test_required_question_bad_answer(self): alert = self.drv.switch_to.alert alert.accept() - self.input_field().send_keys(Keys.BACK_SPACE * 14, '3') + self.input_field().send_keys( + Keys.RIGHT * 14, Keys.BACK_SPACE * 14, '3' + ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) @@ -2254,10 +2256,10 @@ def test_allow_multiple_cant_fool_required(self): self.sleep() self.drv.find_elements_by_tag_name('input')[0].send_keys( - Keys.BACK_SPACE * 14, '3' + Keys.RIGHT * 15, Keys.BACK_SPACE * 15, '3' ) self.drv.find_elements_by_tag_name('input')[-1].send_keys( - Keys.BACK_SPACE * 13, '4' + Keys.RIGHT * 14, Keys.BACK_SPACE * 14, '4' ) self.click(self.drv.find_element_by_class_name('navigate-right')) From b18490c0bb3156e054239e552b59eab54f0c4908 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 23 Nov 2015 15:30:53 -0500 Subject: [PATCH 31/69] Date/time/timestamp input fixes for Android Selenium --- .travis.yml | 2 +- tests/python/test_selenium.py | 259 +++++++++++++++------------------- tox.ini | 1 + 3 files changed, 119 insertions(+), 143 deletions(-) diff --git a/.travis.yml b/.travis.yml index d55aa49f..a46a9f38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ addons: firefox: "42.0" before_install: - - pip install coveralls flake8 coverage beautifulsoup4 py-dateutil selenium + - pip install coveralls flake8 coverage beautifulsoup4 py-dateutil pytz selenium before_script: - python3 -m flake8 . diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index d882066d..f5e89ad5 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -18,10 +18,12 @@ from bs4 import BeautifulSoup import dateutil.parser -from dateutil.tz import tzlocal +from dateutil.tz import tzlocal, tzoffset from passlib.hash import bcrypt_sha256 +import pytz + from selenium import webdriver from selenium.common.exceptions import ( TimeoutException, ElementNotVisibleException, WebDriverException @@ -319,6 +321,16 @@ def enter_date(self, element, year, month, day): self.sleep() element.send_keys(year) self.sleep() + elif self.browser == 'android': + self.drv.execute_script( + 'arguments[0].value = "{}-{}-{}"'.format(year, month, day), + element + ) + self.drv.execute_script( + "var event = new Event('input', {bubbles: true}); " + "arguments[0].dispatchEvent(event);", + element + ) else: element.send_keys('/'.join((year, month, day))) @@ -330,27 +342,95 @@ def enter_time(self, element, hour, minute, am_pm): self.sleep() element.send_keys(am_pm) self.sleep() + elif self.browser == 'android': + if hour == '12' and am_pm == 'AM': + adjusted_hour = '0' + elif hour != '12' and am_pm == 'PM': + adjusted_hour = str(int(hour) + 12) + else: + adjusted_hour = hour + + h = adjusted_hour + adjusted_hour = '0' + h if len(h) == 1 else h + adjusted_minute = '0' + minute if len(minute) == 1 else minute + + self.drv.execute_script( + 'arguments[0].value = "{}:{}"'.format( + adjusted_hour, adjusted_minute + ), + element + ) + self.drv.execute_script( + "var event = new Event('input', {bubbles: true}); " + "arguments[0].dispatchEvent(event);", + element + ) else: element.send_keys('{}:{} {}'.format(hour, minute, am_pm)) - def enter_timestamp(self, element, year, month, day, hour, minute, am_pm): + def enter_timestamp(self, element, timestamp): + modified_time_str = self.drv.execute_script( + 'return moment("{}").format();'.format(timestamp) + ) + modified_time = dateutil.parser.parse(modified_time_str) + unmodified_time = ( + dateutil.parser.parse(timestamp).replace(tzinfo=pytz.utc) + ) + utc_offset = (modified_time - unmodified_time).seconds + utc_offset_time = ( + dateutil.parser. + parse(timestamp). + replace(tzinfo=tzoffset(None, utc_offset)) + ) + utc_time = utc_offset_time.astimezone(pytz.utc) + + if self.browser == 'android': + self.drv.execute_script( + 'arguments[0].value = "{}"'.format( + utc_time.isoformat()[:-6] + ), + element + ) + self.sleep() + self.drv.execute_script( + "var event = new Event('input', {bubbles: true}); " + "arguments[0].dispatchEvent(event);", + element + ) + self.sleep() + self.click(element) + if 'NATIVE_APP' in self.drv.window_handles: + self.drv.switch_to.window('NATIVE_APP') + buttons = self.drv.find_elements_by_tag_name('Button') + if buttons: + self.click(buttons[1]) + self.drv.switch_to.window('WEBVIEW_0') + self.drv.execute_script( + "var event = new Event('input', {bubbles: true}); " + "arguments[0].dispatchEvent(event);", + self.input_field() + ) + self.sleep() + return if self.browser == 'chrome': raise unittest.SkipTest('Selenium + Chrome + timestamp == 😢') - self.enter_date(element, year, month, day) + self.enter_date( + element, + utc_time.strftime('%Y'), + utc_time.strftime('%m'), + utc_time.strftime('%d'), + ) if self.browser == 'chrome': # For some reason this doesn't work... element.send_keys(Keys.TAB) else: element.send_keys(' ') - self.enter_time(element, hour, minute, am_pm) - - def enter_timestamp_temporary(self, e, y, mo, d, h, mi, am_pm): - """Use this temporarily until we use moment.js.""" - if self.browser == 'chrome': - raise unittest.SkipTest('Selenium + Chrome + timestamp == 😢') - e.send_keys('{}-{}-{}T{}:{}:00Z'.format( - y, mo, d, h if am_pm.lower().startswith('a') else h + 12, mi - )) + self.enter_time( + element, + utc_time.strftime('%I'), + utc_time.strftime('%M'), + utc_time.strftime('%p'), + ) class TestAuth(DriverTest): @@ -1568,7 +1648,7 @@ def test_single_timestamp_question(self): self.click(self.drv.find_element_by_class_name('navigate-right')) self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2015', '08', '11', '3', '33', 'PM' + '2015-08-11T15:33:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) @@ -1578,12 +1658,7 @@ def test_single_timestamp_question(self): self.assertIsNot(existing_submission, new_submission) answer = new_submission.answers[0].answer - date_answer = answer.date() - self.assertEqual(date_answer.isoformat(), '2015-08-11') - time_answer = answer.timetz() - answer_parts = re.split('[-+]', time_answer.isoformat()) - self.assertEqual(len(answer_parts), 2, msg=answer_parts) - self.assertEqual(answer_parts[0], '15:33:00') + self.assertEqual(answer.isoformat(), '2015-08-11T15:33:00+00:00') @report_success_status def test_single_location_question(self): @@ -3254,17 +3329,7 @@ def test_date_buckets(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2015', '01', '04' @@ -3275,17 +3340,7 @@ def test_date_buckets(self): 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2015', '01', '01' @@ -3317,17 +3372,7 @@ def test_date_buckets_open_ranges(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2015', '11', '22' @@ -3338,17 +3383,7 @@ def test_date_buckets_open_ranges(self): 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2015', '01', '05' @@ -3376,17 +3411,7 @@ def test_date_buckets_total_open(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.BACK_SPACE * 10) self.enter_date( self.drv.find_element_by_tag_name('input'), '2070', '01', '05' @@ -3401,16 +3426,16 @@ def test_date_buckets_total_open(self): def test_timestamp_buckets(self): survey_id = self.survey_with_branch( 'timestamp', - '(2015-01-01T1:00:00Z, 2015-01-03:1:00:00Z)', - '[2015-01-04T1:00:00Z, 2015-01-05T1:00:00Z]' + '(2015-01-01T12:00:00Z, 2015-01-03T12:00:00Z)', + '[2015-01-04T12:00:00Z, 2015-01-05T12:00:00Z]' ) self.get('/enumerate/{}'.format(survey_id)) self.wait_for_element('navigate-right', By.CLASS_NAME) self.click(self.drv.find_element_by_class_name('navigate-right')) - self.enter_timestamp_temporary( + self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2015', '01', '02', '01', '00', 'AM' + '2015-01-02T12:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3418,20 +3443,10 @@ def test_timestamp_buckets(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) - self.enter_timestamp_temporary( + self.input_field().send_keys(Keys.RIGHT * 30, Keys.BACK_SPACE * 30) + self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2015', '01', '04', '01', '00', 'AM' + '2015-01-04T12:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3439,20 +3454,10 @@ def test_timestamp_buckets(self): 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) - self.enter_timestamp_temporary( + self.input_field().send_keys(Keys.RIGHT * 30, Keys.BACK_SPACE * 30) + self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2015', '01', '01', '01', '00', 'AM' + '2015-01-01T12:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3473,7 +3478,7 @@ def test_timestamp_buckets_open_ranges(self): self.click(self.drv.find_element_by_class_name('navigate-right')) self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2014', '11', '22', '01', '00', 'AM' + '2014-11-22T01:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3481,20 +3486,10 @@ def test_timestamp_buckets_open_ranges(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.RIGHT * 30, Keys.BACK_SPACE * 30) self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2015', '11', '22', '01', '00', 'AM' + '2015-11-22T01:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3502,20 +3497,10 @@ def test_timestamp_buckets_open_ranges(self): 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.RIGHT * 30, Keys.BACK_SPACE * 30) self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2015', '01', '05', '01', '00', 'AM' + '2015-01-05T01:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3532,7 +3517,7 @@ def test_timestamp_buckets_total_open(self): self.click(self.drv.find_element_by_class_name('navigate-right')) self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '1970', '01', '05', '01', '00', 'AM' + '1970-01-05T01:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( @@ -3540,20 +3525,10 @@ def test_timestamp_buckets_total_open(self): 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - ( - ActionChains(self.drv) - .key_down( - self.control_key, - self.drv.find_element_by_tag_name('input') - ) - .send_keys('a') - .key_up(self.control_key) - .send_keys(Keys.DELETE) - .perform() - ) + self.input_field().send_keys(Keys.RIGHT * 30, Keys.BACK_SPACE * 30) self.enter_timestamp( self.drv.find_element_by_tag_name('input'), - '2070', '01', '05', '01', '00', 'AM' + '2070-01-05T01:00:00' ) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( diff --git a/tox.ini b/tox.ini index cdd9a0ce..d8db2af6 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ deps= -rrequirements.txt beautifulsoup4 py-dateutil + pytz selenium passenv=TRAVIS DISPLAY SAUCE_CONNECT SAUCE_USERNAME SAUCE_ACCESS_KEY BROWSER DB_PORT_5432_TCP_ADDR DB_PORT_5432_TCP_PORT POSTGRES_PASSWORD POSTGRES_DB DB_ENV_POSTGRES_PASSWORD DB_DEV_PORT_5432_TCP_ADDR DB_DEV_PORT_5432_TCP_PORT DB_DEV_ENV_POSTGRES_PASSWORD From d2740a01c75ebef310c08191246adbb46b98b15b Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 24 Nov 2015 12:08:19 -0500 Subject: [PATCH 32/69] Skip Sauce Labs tests properly --- tests/python/test_selenium.py | 47 ++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index f5e89ad5..4d391354 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -119,6 +119,25 @@ def start_remote_webdriver(self): finally: signal.alarm(0) + @classmethod + def setUpClass(cls): + if not SAUCE_CONNECT: + return + cls.username = os.environ.get('SAUCE_USERNAME', SAUCE_USERNAME) + cls.access_key = os.environ.get('SAUCE_ACCESS_KEY', SAUCE_ACCESS_KEY) + cls.browser_config = os.environ.get('BROWSER', DEFAULT_BROWSER) + values = (cls.username, cls.access_key, cls.browser_config) + if any(v is None for v in values): + cls.fail( + cls, + 'You have specified SAUCE_CONNECT=true but you have not' + ' specified SAUCE_USERNAME, SAUCE_ACCESS_KEY,' + ' and DEFAULT_BROWSER' + ) + configs = cls.browser_config.split(':') + cls.browser, cls.version, cls.platform, *cls.other = configs + super().setUpClass() + def setUp(self): try: urlopen(base) @@ -143,25 +162,13 @@ def setUp(self): self.platform = 'Linux' return - self.username = os.environ.get('SAUCE_USERNAME', SAUCE_USERNAME) - self.access_key = os.environ.get('SAUCE_ACCESS_KEY', SAUCE_ACCESS_KEY) - browser_config = os.environ.get('BROWSER', DEFAULT_BROWSER) - values = (self.username, self.access_key, browser_config) - if any(v is None for v in values): - self.fail( - 'You have specified SAUCE_CONNECT=true but you have not' - ' specified SAUCE_USERNAME, SAUCE_ACCESS_KEY,' - ' and DEFAULT_BROWSER' - ) - configs = browser_config.split(':') - self.browser, self.version, self.platform, *other = configs caps = { 'browserName': self.browser, 'platform': self.platform, 'idleTimeout': 1000, # maximum } if self.browser in {'android', 'iPhone'}: - caps['deviceName'] = other[0] + caps['deviceName'] = self.other[0] caps['device-orientation'] = 'portrait' if self.version: caps['version'] = self.version @@ -172,13 +179,13 @@ def setUp(self): caps['tags'] = [os.environ['TRAVIS_PYTHON_VERSION'], 'CI'] caps['name'] = ' -- '.join(( os.environ['TRAVIS_BUILD_NUMBER'], - browser_config, + self.browser_config, '{}.{}'.format(self.__class__.__name__, self._testMethodName) )) else: caps['name'] = ' -- '.join(( 'Manual run', - browser_config, + self.browser_config, '{}.{}'.format(self.__class__.__name__, self._testMethodName) )) hub_url = '{}:{}@localhost:4445'.format(self.username, self.access_key) @@ -455,10 +462,16 @@ def test_login(self): class AdminTest(DriverTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + if cls.browser in {'android', 'iPhone'}: + cls.skipTest( + cls, 'The admin interface has no mobile design (yet).' + ) + def setUp(self): super().setUp() - if self.browser in {'android', 'iPhone'}: - self.skipTest('The admin interface has no mobile design (yet).') self.get('/debug/login/test_creator@fixtures.com') self.wait_for_element('html', by=By.TAG_NAME) From 49e9f14e9b51e7169b9419d799036ef52475cbc6 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 24 Nov 2015 13:28:10 -0500 Subject: [PATCH 33/69] Fix selenium tests --- tests/python/test_selenium.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 4d391354..57112e65 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -122,6 +122,8 @@ def start_remote_webdriver(self): @classmethod def setUpClass(cls): if not SAUCE_CONNECT: + cls.browser = 'firefox' + cls.platform = 'Linux' return cls.username = os.environ.get('SAUCE_USERNAME', SAUCE_USERNAME) cls.access_key = os.environ.get('SAUCE_ACCESS_KEY', SAUCE_ACCESS_KEY) @@ -158,8 +160,6 @@ def setUp(self): if not SAUCE_CONNECT: self.drv = webdriver.Firefox(firefox_profile=f_profile) - self.browser = 'firefox' - self.platform = 'Linux' return caps = { @@ -1139,9 +1139,7 @@ def test_change_language(self): )) save_btn = self.drv.find_element_by_class_name('btn-save-user') - self.sleep() - save_btn.click() - self.sleep() + self.click(save_btn) # refresh the page self.drv.refresh() From 074235e3c1672d9c30cd79945604dff4058708c3 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 24 Nov 2015 16:15:14 -0500 Subject: [PATCH 34/69] This might make things more reliable. --- dokomoforms/handlers/api/v0/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dokomoforms/handlers/api/v0/base.py b/dokomoforms/handlers/api/v0/base.py index cd508cd7..6ed2da14 100644 --- a/dokomoforms/handlers/api/v0/base.py +++ b/dokomoforms/handlers/api/v0/base.py @@ -283,6 +283,7 @@ def list(self, where=None): Given a model class, build up the ORM query based on query params and return the query result. """ + self.session.flush() model_cls = self.resource_type query = self.session.query(model_cls, count().over()) From 8f06465e47c3228c37e62bd6270f822798d75934 Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Wed, 25 Nov 2015 13:00:43 -0500 Subject: [PATCH 35/69] fixed css issue (bootstrap variable change) --- dokomoforms/static/src/admin/less/variables.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dokomoforms/static/src/admin/less/variables.less b/dokomoforms/static/src/admin/less/variables.less index 58373b7e..cce117ef 100755 --- a/dokomoforms/static/src/admin/less/variables.less +++ b/dokomoforms/static/src/admin/less/variables.less @@ -863,5 +863,7 @@ @page-header-border-color: @gray-lighter; //** Width of horizontal description list titles @dl-horizontal-offset: @component-offset-horizontal; +//** Point at which .dl-horizontal becomes horizontal +@dl-horizontal-breakpoint: @grid-float-breakpoint; //** Horizontal line color. @hr-border: @gray-lighter; From cdc2d3c356965d9176f354f27101a05fc6e3ba77 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 30 Nov 2015 15:39:07 -0500 Subject: [PATCH 36/69] Fix test_add_new_facility on Android 4.4 --- tests/python/test_selenium.py | 38 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 57112e65..b2cc2877 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -439,6 +439,30 @@ def enter_timestamp(self, element, timestamp): utc_time.strftime('%p'), ) + def select_by_index(self, element, index): + """Select an element from a <select> <option> list. + + For some reason on Android selecting an option works but raises an + exception.... + So... ingnore the exception. + """ + andr = self.browser == 'android' + if andr and self.version < StrictVersion('5.0'): + self.click(self.drv.find_element_by_tag_name('select')) + self.drv.switch_to.window('NATIVE_APP') + self.click( + self.drv.find_elements_by_tag_name('TextView')[index + 2] + ) + self.drv.switch_to.window('WEBVIEW_0') + return + try: + element.select_by_index(index) + except WebDriverException: + if andr: + pass + else: + raise + class TestAuth(DriverTest): @report_success_status @@ -1374,16 +1398,7 @@ def test_change_language(self): self.click(self.drv.find_element_by_class_name('menu')) lang = Select(self.drv.find_element_by_class_name('language_select')) self.assertEqual(len(lang.options), 3) - # For some reason on Android selecting an option works but raises an - # exception... - # So... ignore the exception! - try: - lang.select_by_index(1) - except WebDriverException: - if self.browser == 'android': - pass - else: - raise + self.select_by_index(lang, 1) self.sleep() @@ -4008,7 +4023,8 @@ def test_add_new_facility(self): ) # navigate to end of survey and save - self.click(self.drv.find_elements_by_tag_name('option')[1]) + facility_type = Select(self.drv.find_element_by_tag_name('select')) + self.select_by_index(facility_type, 1) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) From 9fc7c81281ce63c1b2d82ead810f622a69c9ee25 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 30 Nov 2015 15:48:54 -0500 Subject: [PATCH 37/69] Fix tests_add_new_facility_revisit_cuts_out on Android 4.4 --- tests/python/test_selenium.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index b2cc2877..dde72baa 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -4081,7 +4081,8 @@ def test_add_new_facility_revisit_cuts_out(self): .send_keys('new facility') ) # navigate to end of survey and save - self.click(self.drv.find_elements_by_tag_name('option')[1]) + facility_type = Select(self.drv.find_element_by_tag_name('select')) + self.select_by_index(facility_type, 1) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) From edd869da044ce7b9f3b600e5786f6014a9772bb6 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 30 Nov 2015 17:08:46 -0500 Subject: [PATCH 38/69] Fix tests_connectivity_cuts_out on Android 4.4 --- tests/python/test_selenium.py | 42 +++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index dde72baa..c913d97d 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -1318,7 +1318,7 @@ def get_single_node_survey_id(self, question_type): def get_last_submission(self, survey_id): self.sleep() - return ( + result = ( self.session .query(Submission) .filter_by(survey_id=survey_id) @@ -1326,6 +1326,25 @@ def get_last_submission(self, survey_id): .limit(1) .one() ) + andr = self.browser == 'android' + if andr and self.version < StrictVersion('5.0'): + # For some reason the Android 4.4 tests report the wrong time + # for a while... This gets around that problem. + try: + existing = self.existing + except AttributeError: + self.existing = result + else: + result = ( + self.session + .query(Submission) + .filter(Submission.id != existing.id) + .filter(Submission.survey_id == survey_id) + .order_by(Submission.save_time.desc()) + .limit(1) + .one() + ) + return result @report_success_status def test_login(self): @@ -4250,7 +4269,7 @@ def get_single_node_survey_id(self, question_type): def get_last_submission(self, survey_id): self.sleep() - return ( + result = ( self.session .query(Submission) .filter_by(survey_id=survey_id) @@ -4258,6 +4277,25 @@ def get_last_submission(self, survey_id): .limit(1) .one() ) + andr = self.browser == 'android' + if andr and self.version < StrictVersion('5.0'): + # For some reason the Android 4.4 tests report the wrong time + # for a while... This gets around that problem. + try: + existing = self.existing + except AttributeError: + self.existing = result + else: + result = ( + self.session + .query(Submission) + .filter(Submission.id != existing.id) + .filter(Submission.survey_id == survey_id) + .order_by(Submission.save_time.desc()) + .limit(1) + .one() + ) + return result @report_success_status def test_revisit_offline_entirely(self): From 7696419d842cddef83afcbc59d8db541d4bcfe1a Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 13:12:43 -0500 Subject: [PATCH 39/69] Fix tests_multiple_choice_buckets on Android 4.4 --- tests/python/test_selenium.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index c913d97d..75344d27 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -3636,24 +3636,26 @@ def test_multiple_choice_buckets(self): survey_id = survey.id + element_by_tag = self.drv.find_element_by_tag_name + self.get('/enumerate/{}'.format(survey_id)) self.wait_for_element('navigate-right', By.CLASS_NAME) self.click(self.drv.find_element_by_class_name('navigate-right')) - self.click(self.drv.find_elements_by_tag_name('option')[1]) + self.select_by_index(Select(element_by_tag('select')), 1) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, 'b0' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - self.click(self.drv.find_elements_by_tag_name('option')[2]) + self.select_by_index(Select(element_by_tag('select')), 2) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, 'b1' ) self.click(self.drv.find_element_by_class_name('page_nav__prev')) - self.click(self.drv.find_elements_by_tag_name('option')[3]) + self.select_by_index(Select(element_by_tag('select')), 3) self.click(self.drv.find_element_by_class_name('navigate-right')) self.assertEqual( self.drv.find_element_by_tag_name('h3').text, From 20a50d430e342a9b7ee27bd93901a7d3d13883f7 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 14:00:54 -0500 Subject: [PATCH 40/69] Fix tests_other on Android 4.4 --- tests/python/test_selenium.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 75344d27..1b4269d1 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -1898,7 +1898,8 @@ def test_other(self): self.wait_for_element('navigate-right', By.CLASS_NAME) self.click(self.drv.find_element_by_class_name('navigate-right')) - self.click(self.drv.find_elements_by_tag_name('option')[-1]) + e_by_tag = self.drv.find_element_by_tag_name + self.select_by_index(Select(e_by_tag('select')), 3) ( self.drv .find_element_by_tag_name('input') From 029b77f92a3859229bb2efc5931d08ed821927b1 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 14:34:42 -0500 Subject: [PATCH 41/69] Fix tests_select_multiple on Android 4.4 --- tests/python/test_selenium.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 1b4269d1..f6914f10 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -463,6 +463,20 @@ def select_by_index(self, element, index): else: raise + def select_multiple(self, *indices): + if self.browser == 'android' and self.version < StrictVersion('5.0'): + self.click(self.drv.find_element_by_tag_name('select')) + self.drv.switch_to.window('NATIVE_APP') + options = self.drv.find_elements_by_tag_name('CheckedTextView') + for index in indices: + self.click(options[index]) + self.click(self.drv.find_elements_by_tag_name('Button')[-1]) + self.drv.switch_to.window('WEBVIEW_0') + return + options = self.drv.find_elements_by_tag_name('option') + for index in indices: + self.click(options[index]) + class TestAuth(DriverTest): @report_success_status @@ -1846,8 +1860,7 @@ def test_select_multiple(self): self.wait_for_element('navigate-right', By.CLASS_NAME) self.click(self.drv.find_element_by_class_name('navigate-right')) - self.click(self.drv.find_elements_by_tag_name('option')[1]) - self.click(self.drv.find_elements_by_tag_name('option')[2]) + self.select_multiple(1, 2) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) @@ -4170,7 +4183,8 @@ def test_revisit_offline_then_online(self): .send_keys('new facility') ) # navigate to end of survey and save - self.click(self.drv.find_elements_by_tag_name('option')[1]) + facility_type = Select(self.drv.find_element_by_tag_name('select')) + self.select_by_index(facility_type, 1) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) From 0ef6d153e761be4810c1f97aef392ef2c6963657 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 14:53:57 -0500 Subject: [PATCH 42/69] Fix test_single_multiple_choice_question on Android 4.4 --- tests/python/test_selenium.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index f6914f10..e4bacc6c 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -1813,7 +1813,8 @@ def test_single_multiple_choice_question(self): self.wait_for_element('navigate-right', By.CLASS_NAME) self.click(self.drv.find_element_by_class_name('navigate-right')) - self.click(self.drv.find_elements_by_tag_name('option')[1]) + e_by_tag = self.drv.find_element_by_tag_name + self.select_by_index(Select(e_by_tag('select')), 1) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) From 9e2e3c218bd2bedcd81db2224a95bb2fa994f69e Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 15:18:55 -0500 Subject: [PATCH 43/69] Skip test_single_photo_question on Android 4.4 --- tests/python/test_selenium.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index e4bacc6c..9ec6421d 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -1621,6 +1621,9 @@ def test_single_text_question(self): @report_success_status def test_single_photo_question(self): + if self.browser == 'android' and self.version < StrictVersion('5.0'): + # http://caniuse.com/#feat=stream + self.skipTest('getUserMedia does not work in the <5 AOSP browser') survey_id = self.get_single_node_survey_id('photo') existing_submission = self.get_last_submission(survey_id) From f61c5e420420558cec495cfae7a1d833b0e066d4 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 15:49:04 -0500 Subject: [PATCH 44/69] Fix enter_timestamp for Android 4.4 --- tests/python/test_selenium.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 9ec6421d..7139d877 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -410,7 +410,9 @@ def enter_timestamp(self, element, timestamp): self.drv.switch_to.window('NATIVE_APP') buttons = self.drv.find_elements_by_tag_name('Button') if buttons: - self.click(buttons[1]) + old_android = self.version < StrictVersion('5.0') + cancel = 0 if old_android else 1 + self.click(buttons[cancel]) self.drv.switch_to.window('WEBVIEW_0') self.drv.execute_script( "var event = new Event('input', {bubbles: true}); " From c9cf5426f059dbf12bace4f0e242ff50f675356c Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 1 Dec 2015 16:07:00 -0500 Subject: [PATCH 45/69] Fix test_revisit_offline_entirely on Android 4.4 --- tests/python/test_selenium.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 7139d877..6c0ff573 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -4352,7 +4352,8 @@ def test_revisit_offline_entirely(self): .send_keys('new facility') ) # navigate to end of survey and save - self.click(self.drv.find_elements_by_tag_name('option')[1]) + facility_type = Select(self.drv.find_element_by_tag_name('select')) + self.select_by_index(facility_type, 1) self.click(self.drv.find_element_by_class_name('navigate-right')) self.click(self.drv.find_element_by_class_name('navigate-right')) From c123771f5f8bc704b1a12a6838227d78b245d2b4 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Thu, 3 Dec 2015 16:24:17 -0500 Subject: [PATCH 46/69] This seems to help --- tests/python/test_selenium.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 6c0ff573..44e061ac 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -1426,6 +1426,7 @@ def test_change_language(self): # login as enumerator self.get('/debug/login/test_enumerator@fixtures.com') + self.sleep() self.get('/enumerate/{}'.format(survey_id)) From 5f01c408ab6db9ac64598bc2429a1b15c076da3e Mon Sep 17 00:00:00 2001 From: Jonathan Wohl <jon@jonwohl.com> Date: Sat, 5 Dec 2015 07:43:17 -0600 Subject: [PATCH 47/69] use ssl for revisit --- dokomoforms/options.py | 2 +- local_config.py.example | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dokomoforms/options.py b/dokomoforms/options.py index fe8b1366..a692e15a 100644 --- a/dokomoforms/options.py +++ b/dokomoforms/options.py @@ -34,7 +34,7 @@ persona_url = 'https://verifier.login.persona.org/verify' define('persona_verification_url', default=persona_url, help=persona_help) -revisit_url = 'http://revisit.global/api/v0/facilities.json' +revisit_url = 'https://revisit.global/api/v0/facilities.json' revisit_help = ( 'the URL for facility data. Do not change this without a good reason.' ) diff --git a/local_config.py.example b/local_config.py.example index d87ad0dc..31999251 100644 --- a/local_config.py.example +++ b/local_config.py.example @@ -76,7 +76,7 @@ import os # revisit_url = 'some URL' -# Default: 'http://revisit.global/api/v0/facilities.json' +# Default: 'https://revisit.global/api/v0/facilities.json' # Set revisit_url to tell the application which Revisit server to use as a # registry for facility data. By default it will use the official # revisit.global server. You may wish to use staging.revisit.global instead From b7fe8a7a03b77116fb2f7fa190ab50667e4ef3cf Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 7 Dec 2015 11:13:06 -0500 Subject: [PATCH 48/69] Make the Revisit slow mode tests clearer --- dokomoforms/handlers/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dokomoforms/handlers/debug.py b/dokomoforms/handlers/debug.py index 7fc8fced..51b05f70 100644 --- a/dokomoforms/handlers/debug.py +++ b/dokomoforms/handlers/debug.py @@ -125,7 +125,7 @@ def get(self): if not revisit_online: raise tornado.web.HTTPError(502) if slow_mode: # pragma: no cover - sleep(2) + sleep(2.5) self.write(compressed_facilities) self.set_header('Content-Type', 'application/json') From 89293cd1b99f66c82d2b39e7136c040cae82dc45 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 7 Dec 2015 12:15:08 -0500 Subject: [PATCH 49/69] Change test command order --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a46a9f38..d428abed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,9 @@ before_script: - nvm install stable - npm install npm -g - npm install + - node_modules/gulp/bin/gulp.js dev-build - ./tests/python/selenium_webapp.py &>/dev/null & - sleep 2 - - node_modules/gulp/bin/gulp.js dev-build script: - npm test From 7ca6f40cbcfa903ee9da421ed5105efb089d1148 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 7 Dec 2015 16:30:48 -0500 Subject: [PATCH 50/69] Fix Sauce Labs tests for weird date issue. --- tests/python/test_selenium.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 44e061ac..084cf80f 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -18,7 +18,7 @@ from bs4 import BeautifulSoup import dateutil.parser -from dateutil.tz import tzlocal, tzoffset +from dateutil.tz import tzoffset from passlib.hash import bcrypt_sha256 @@ -920,9 +920,12 @@ def test_manage_renders_properly(self): .filter_by(id='b0816b52-204f-41d4-aaf0-ac6ae2970923') .scalar() ) + earliest_local = self.drv.execute_script( + 'return moment("{}").format();'.format(earliest_utc) + ) self.assertEqual( dateutil.parser.parse(stats[1].text).date(), - earliest_utc.astimezone(tzlocal()).date() + dateutil.parser.parse(earliest_local).date() ) self.assertEqual( dateutil.parser.parse(stats[2].text).date(), @@ -1311,9 +1314,12 @@ def test_view_data_renders_properly(self): .filter_by(id='b0816b52-204f-41d4-aaf0-ac6ae2970923') .scalar() ) + earliest_local = self.drv.execute_script( + 'return moment("{}").format();'.format(earliest_utc) + ) self.assertEqual( dateutil.parser.parse(stats[1].text).date(), - earliest_utc.astimezone(tzlocal()).date() + dateutil.parser.parse(earliest_local).date() ) self.assertEqual( dateutil.parser.parse(stats[2].text).date(), From 46680e6b95e6da845eee95242d9290516907e970 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Wed, 9 Dec 2015 14:57:02 -0500 Subject: [PATCH 51/69] Update readthedocs badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8810f46f..a2d980db 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Dokomo Forms is a free and open source data collection and analysis platform. [![Build Status](https://travis-ci.org/SEL-Columbia/dokomoforms.svg?branch=master)](https://travis-ci.org/SEL-Columbia/dokomoforms) [![Coverage Status](https://coveralls.io/repos/SEL-Columbia/dokomoforms/badge.svg?branch=master)](https://coveralls.io/r/SEL-Columbia/dokomoforms?branch=master) -[![Documentation Status](https://readthedocs.org/projects/dokomoforms/badge/?version=latest)](https://readthedocs.org/projects/dokomoforms/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/dokomoforms/badge/?version=master)](https://readthedocs.org/projects/dokomoforms/?badge=latest) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SEL-Columbia/dokomoforms?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Dependency Status](https://gemnasium.com/SEL-Columbia/dokomoforms.svg)](https://gemnasium.com/SEL-Columbia/dokomoforms) [![Sauce Test Status](https://saucelabs.com/browser-matrix/dokomo_sauce_matrix.svg)](https://saucelabs.com/u/dokomo_sauce_matrix) From 34647e5bcae4773d5276fba2a25c15f1a789f413 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Wed, 9 Dec 2015 16:33:38 -0500 Subject: [PATCH 52/69] Bump nginx version --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 900ba628..a77a644e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ nginx: - image: "nginx:1.9.5" + image: "nginx:1.9.8" links: - "webapp:webapp" ports: From c5850130df905c5100bd865f34fdf427fbda093e Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Thu, 10 Dec 2015 10:14:44 -0500 Subject: [PATCH 53/69] First steps to https --- config.py | 2 -- docker-compose.yml | 3 +++ dokomoforms/options.py | 2 +- local_config.py.example | 5 +++++ nginx.conf | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 88d3d7a4..9d9782bd 100755 --- a/config.py +++ b/config.py @@ -31,8 +31,6 @@ db_user = 'postgres' organization = 'unconfigured organization' -https = True - try: from local_config import * # NOQA except ImportError: diff --git a/docker-compose.yml b/docker-compose.yml index a77a644e..47eb3c79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,11 @@ nginx: - "webapp:webapp" ports: - "80:80" + - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf + - /etc/letsencrypt:/etc/letsencrypt + - /tmp:/tmp webapp: image: "selcolumbia/dokomoforms" command: bash -c "./docker-wait-for-postgres.sh db && head -c 24 /dev/urandom > cookie_secret && python webapp.py" diff --git a/dokomoforms/options.py b/dokomoforms/options.py index a692e15a..5d72f33b 100644 --- a/dokomoforms/options.py +++ b/dokomoforms/options.py @@ -24,7 +24,7 @@ define('debug', default=False, help='whether to enable debug mode', type=bool) https_help = 'whether the application accepts https traffic' -define('https', help=https_help, type=bool) +define('https', default=True, help=https_help, type=bool) define('organization', help='the name of your organization') diff --git a/local_config.py.example b/local_config.py.example index 31999251..2348e789 100644 --- a/local_config.py.example +++ b/local_config.py.example @@ -62,6 +62,11 @@ import os # Default: True # Set https = False to put the webapp into HTTP mode (can not serve HTTPS # traffic). +# +# WARNING: +# Some of the features of the application will only work if served over HTTPS. +# Do not set https = False unless you want to develop the application without +# bothering with a dummy self-signed certificate. ####################################### diff --git a/nginx.conf b/nginx.conf index fd79954a..fdd37f3a 100644 --- a/nginx.conf +++ b/nginx.conf @@ -8,6 +8,22 @@ http { server { listen 80; listen [::]:80; + server_name www.example.com; + rewrite ^/(.*) https://$server_name/$1 permanent; + } + server { + server_name www.example.com; + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl_dhparam /etc/letsencrypt/live/www.example.com/dhparam.pem; + ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; + + location '/.well-known/acme-challenge' { + default_type "text/plain"; + root /tmp/letsencrypt-auto; + } + location / { proxy_pass_header Server; proxy_set_header Host $http_host; From fe04e360aff9aa179db5d878225a7a669e7f8cbc Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Thu, 10 Dec 2015 12:18:50 -0500 Subject: [PATCH 54/69] Added Let's Encrypt configuration file --- cli.ini | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 cli.ini diff --git a/cli.ini b/cli.ini new file mode 100644 index 00000000..a7546354 --- /dev/null +++ b/cli.ini @@ -0,0 +1,31 @@ +# This is an example of the kind of things you can do in a configuration file. +# All flags used by the client can be configured here. Run Let's Encrypt with +# "--help" to learn more about the available options. + +# Use a 4096 bit RSA key instead of 2048 +# rsa-key-size = 4096 + +# Always use the staging/testing server +# server = https://acme-staging.api.letsencrypt.org/directory + +# Uncomment and update to register with the specified e-mail address +email = email@example.com + +# Uncomment and update to generate certificates for the specified +# domains. +domains = www.example.com + +# Uncomment to use a text interface instead of ncurses +# text = True + +# Uncomment to use the standalone authenticator on port 443 +# authenticator = standalone +# standalone-supported-challenges = tls-sni-01 + +# Uncomment to use the webroot authenticator. Replace webroot-path with the +# path to the public_html / webroot folder being served by your web server. +authenticator = webroot +webroot-path = /tmp/letsencrypt-auto + +agree-tos = True +renew = True From 10b424ea8ecc04642e4f225b2ffc230219ca195d Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Thu, 10 Dec 2015 12:42:49 -0500 Subject: [PATCH 55/69] Improve SSL configuration in nginx.conf --- nginx.conf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nginx.conf b/nginx.conf index fdd37f3a..d8b0cbc8 100644 --- a/nginx.conf +++ b/nginx.conf @@ -15,9 +15,20 @@ http { server_name www.example.com; listen 443 ssl http2; listen [::]:443 ssl http2; + ssl_dhparam /etc/letsencrypt/live/www.example.com/dhparam.pem; ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; + ssl_prefer_server_ciphers on; + add_header Strict-Transport-Security max-age=15768000; + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /etc/letsencrypt/live/www.example.com/chain.pem; location '/.well-known/acme-challenge' { default_type "text/plain"; From b3ff38abe513c23c76f94f069527c1ae2683d55c Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Fri, 11 Dec 2015 10:25:08 -0500 Subject: [PATCH 56/69] First steps to install script --- installer.sh | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100755 installer.sh diff --git a/installer.sh b/installer.sh new file mode 100755 index 00000000..690fc10a --- /dev/null +++ b/installer.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env sh +set -o errexit + +# Do you have docker installed? +if ! command -v docker > /dev/null; then + printf "You need to install docker\n" + exit 1 +fi + +# Do you have sed installed? +if ! command -v sed > /dev/null; then + printf "You need to install sed\n" + exit 1 +fi + +# Do you have openssl installed? +if ! command -v openssl > /dev/null; then + printf "You need to install openssl\n" + exit 1 +fi + +# Do you have curl installed? +if command -v curl > /dev/null; then + CURL=curl +else + CURL="docker run tutum/curl curl" +fi + +# Do you have docker-compose installed? +if command -v docker-compose > /dev/null; then + DOCKER_COMPOSE=docker-compose +else + DOCKER_COMPOSE=./docker-compose + if ! [ -f ./docker-compose ]; then + printf "=========================\n" + printf "Installing docker-compose\n" + printf "=========================\n\n" + $CURL -o docker-compose -L https://github.com/docker/compose/releases/download/1.5.2/run.sh + chmod +x docker-compose + ./docker-compose -v + fi +fi + +# This installer needs root access for various reasons... +# Copy the logic from the letsencrypt-auto script +# https://github.com/letsencrypt/letsencrypt/blob/8c6e242b13ac818c0a94e3dceee81ab4b3816a12/letsencrypt-auto#L34-L68 +if test "`id -u`" -ne "0" ; then + if command -v sudo 1>/dev/null 2>&1; then + SUDO=sudo + else + echo \"sudo\" is not available, will use \"su\" for installation steps... + su_sudo() { + args="" + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift + done + su root -c "$args" + } + SUDO=su_sudo + fi +else + SUDO= +fi + +# Ask for domain(s) +printf "========================================\n" +printf "Please enter your domain name(s) (space \n" +printf "separated) \n" +printf " \n" +printf "Hint: for www include both \n" +printf ">>> www.your.domain your.domain \n" +printf " \n" +printf "For a subdomain just give \n" +printf ">>> subdomain.your.domain \n" +printf "========================================\n" +printf "Domain(s):\n>>> " +read DOMAINS +LETSENCRYPT_DIR=$(echo $DOMAINS | cut -d' ' -f1) +DOMAIN_ARGS=$(echo $DOMAINS | sed -r s/\([^\ ]+\)/-d\ \\1/g) + +# Run letsencrypt +printf "========================================\n" +printf "Installing SSL certificate. Make sure \n" +printf "you have set up the DNS records for your\n" +printf "domain to point to this machine. \n" +printf "========================================\n\n" +$SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ + -v "/etc/letsencrypt:/etc/letsencrypt" \ + -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ + quay.io/letsencrypt/letsencrypt:latest auth $DOMAIN_ARGS + +# Run openssl dhparam +printf "========================================\n" +printf "Generating Diffie-Hellman parameters \n" +printf "using OpenSSL (2048 bit prime) \n" +printf "========================================\n\n" +$SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 2048 + +# Download the configuration files +printf "========================================\n" +printf "Downloading configuration files \n" +printf "========================================\n" +$CURL -O https://raw.githubusercontent.com/SEL-Columbia/dokomoforms/v0.2.2/docker-compose.yml +$CURL -O https://raw.githubusercontent.com/SEL-Columbia/dokomoforms/v0.2.2/nginx.conf + +# Edit the configuration files +printf "========================================\n" +printf "Generating final configuration \n" +printf "========================================\n" + +touch local_config.py + +sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf + +printf "What is the name of your organization?\n" +printf "This will be displayed as part of the title of the website.\n" +printf "Organization name:\n>>> " +read ORGANIZATION +printf "organization = '$ORGANIZATION'\n" >> local_config.py +# To be continued... + +# Let's Encrypt auto-renew (for now this is a cron job). +printf "========================================\n" +printf "Adding monthly cron job to renew SSL \n" +printf "certificate. \n" +printf "========================================\n" +CRON_CMD="mkdir -p /tmp/letsencrypt-auto && docker run -it --rm --name letsencrypt -v /etc/letsencrypt:/etc/letsencrypt -v /var/lib/letsencrypt:/var/lib/letsencrypt -v /tmp/letsencrypt-auto:/tmp/letsencrypt-auto -v /var/log/letsencrypt:/var/log/letsencrypt quay.io/letsencrypt/letsencrypt --renew certonly -a webroot -w /tmp/letsencrypt-auto $DOMAIN_ARGS && docker restart $USER_nginx_1" +CRON_JOB="0 0 1 * * $CRON_CMD" +( crontab -l | fgrep -i -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab - + +# Bring up Dokomo Forms +printf "========================================\n" +printf "Starting Dokomo Forms \n" +printf "========================================\n" +$DOCKER_COMPOSE up -d From 3780cf940c5eaea125a49e35db55c0c51b40113d Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 14 Dec 2015 12:46:00 -0500 Subject: [PATCH 57/69] Remove password environment variable from postgres --- docker-compose-dev.yml | 1 - docker-compose.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 13072eab..1c73a2c4 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -10,5 +10,4 @@ webapp-dev: db-dev: image: "mdillon/postgis:9.4" environment: - POSTGRES_PASSWORD: 'password' POSTGRES_DB: 'doko' diff --git a/docker-compose.yml b/docker-compose.yml index 47eb3c79..39e3dde4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,5 +19,4 @@ webapp: db: image: "mdillon/postgis:9.4" environment: - POSTGRES_PASSWORD: 'password' POSTGRES_DB: 'doko' From a48e0ad55e47dfa96ee5c47a0c7e9cca796e16e4 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 14 Dec 2015 14:07:14 -0500 Subject: [PATCH 58/69] Finishing touches on install script. Minor change to demo mode. --- dokomoforms/handlers/demo.py | 4 +++- installer.sh | 42 ++++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/dokomoforms/handlers/demo.py b/dokomoforms/handlers/demo.py index 2e482bb6..d5cc680e 100644 --- a/dokomoforms/handlers/demo.py +++ b/dokomoforms/handlers/demo.py @@ -3,6 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound +from dokomoforms.options import options import dokomoforms.models as models from dokomoforms.models import Administrator, Email from dokomoforms.handlers.util import BaseHandler @@ -193,9 +194,10 @@ def get(self): except NoResultFound: user = _create_demo_user(self.session) cookie_options = { - 'expires_days': None, 'httponly': True, } + if options.https: + cookie_options['secure'] = True self.set_secure_cookie('user', user.id, **cookie_options) self.redirect('/') diff --git a/installer.sh b/installer.sh index 690fc10a..7769058d 100755 --- a/installer.sh +++ b/installer.sh @@ -1,4 +1,5 @@ #!/usr/bin/env sh +# Dokomo Forms installer for version 0.2.2 set -o errexit # Do you have docker installed? @@ -32,9 +33,10 @@ if command -v docker-compose > /dev/null; then else DOCKER_COMPOSE=./docker-compose if ! [ -f ./docker-compose ]; then - printf "=========================\n" - printf "Installing docker-compose\n" - printf "=========================\n\n" + printf "========================================\n" + printf "Installing docker-compose in this \n" + printf "directory \n" + printf "========================================\n" $CURL -o docker-compose -L https://github.com/docker/compose/releases/download/1.5.2/run.sh chmod +x docker-compose ./docker-compose -v @@ -84,7 +86,7 @@ printf "========================================\n" printf "Installing SSL certificate. Make sure \n" printf "you have set up the DNS records for your\n" printf "domain to point to this machine. \n" -printf "========================================\n\n" +printf "========================================\n" $SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ @@ -94,7 +96,7 @@ $SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ printf "========================================\n" printf "Generating Diffie-Hellman parameters \n" printf "using OpenSSL (2048 bit prime) \n" -printf "========================================\n\n" +printf "========================================\n" $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 2048 # Download the configuration files @@ -113,12 +115,30 @@ touch local_config.py sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf -printf "What is the name of your organization?\n" -printf "This will be displayed as part of the title of the website.\n" +printf "What is the name of your organization? \n" +printf "This will be displayed as part of the \n" +printf "title of the website. \n" printf "Organization name:\n>>> " read ORGANIZATION printf "organization = '$ORGANIZATION'\n" >> local_config.py -# To be continued... + +printf "\n" +printf "Please enter an e-mail address for the \n" +printf "administrator. This will be the only \n" +printf "account that can log in at first. \n" +printf "Administrator e-mail address:\n>>> " +read ADMIN_EMAIL +printf "admin_email = '$ADMIN_EMAIL'\n" >> local_config.py +DEFAULT_NAME=$(echo $ADMIN_EMAIL | cut -d'@' -f1) + +printf "\n" +printf "Please enter a user name for the \n" +printf "administrator. Leave this field blank to\n" +printf "use this user name: \n" +printf "$DEFAULT_NAME\n" +printf "Administrator user name:\n>>> " +read ADMIN_NAME +printf "admin_name = '${ADMIN_NAME:-$DEFAULT_NAME}'\n" >> local_config.py # Let's Encrypt auto-renew (for now this is a cron job). printf "========================================\n" @@ -131,6 +151,10 @@ CRON_JOB="0 0 1 * * $CRON_CMD" # Bring up Dokomo Forms printf "========================================\n" -printf "Starting Dokomo Forms \n" +printf "Starting Dokomo Forms. \n" +printf " \n" +printf "You can view the status of the \n" +printf "containers by running: \n" +printf "$DOCKER_COMPOSE ps\n" printf "========================================\n" $DOCKER_COMPOSE up -d From d32220be1df2c24fae1b0175a47b25a49b4a9d5b Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 14 Dec 2015 16:51:43 -0500 Subject: [PATCH 59/69] Fix user-specific recent submissions --- dokomoforms/static/src/admin/js/account-overview.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dokomoforms/static/src/admin/js/account-overview.js b/dokomoforms/static/src/admin/js/account-overview.js index 44de3a71..cd297f67 100644 --- a/dokomoforms/static/src/admin/js/account-overview.js +++ b/dokomoforms/static/src/admin/js/account-overview.js @@ -70,7 +70,8 @@ var AccountOverview = (function() { function loadRecentSubmissions() { var limit = 5; - return $.getJSON('/api/v0/submissions?order_by=save_time:DESC&limit=' + limit + + return $.getJSON('/api/v0/submissions?user_id=' + window.CURRENT_USER_ID + + '&order_by=save_time:DESC&limit=' + limit + '&fields=id,submission_time,submitter_name,survey_title,survey_id,survey_default_language,answers'); } From ae60706bf38be60927c691d9ae753be7dd41805b Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 14 Dec 2015 16:52:16 -0500 Subject: [PATCH 60/69] Working --- config.py | 3 +++ dokomoforms/handlers/auth.py | 25 ++++++++++++++++--------- dokomoforms/options.py | 2 ++ local_config.py.example | 17 +++++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/config.py b/config.py index 9d9782bd..591a71d4 100755 --- a/config.py +++ b/config.py @@ -29,7 +29,10 @@ db_database = 'doko' db_user = 'postgres' + organization = 'unconfigured organization' +admin_email = 'admin@example.com' +admin_name = 'admin' try: from local_config import * # NOQA diff --git a/dokomoforms/handlers/auth.py b/dokomoforms/handlers/auth.py index 6dd123a8..398fef3e 100644 --- a/dokomoforms/handlers/auth.py +++ b/dokomoforms/handlers/auth.py @@ -17,7 +17,7 @@ from dokomoforms.options import options from dokomoforms.handlers.util import BaseHandler, authenticated_admin -from dokomoforms.models import User, Email +from dokomoforms.models import User, Administrator, Email class Login(BaseHandler): @@ -79,14 +79,21 @@ def post(self): .one() ) except NoResultFound: - _ = self.locale.translate - raise tornado.web.HTTPError( - 422, - reason=_( - 'There is no account associated with the e-mail' - ' address {}'.format(data['email']) - ), - ) + if data['email'] != options.admin_email: + _ = self.locale.translate + raise tornado.web.HTTPError( + 422, + reason=_( + 'There is no account associated with the e-mail' + ' address {}'.format(data['email']) + ), + ) + with self.session.begin(): + user = Administrator( + name=options.admin_name, + emails=[Email(address=options.admin_email)] + ) + self.session.add(user) cookie_options = { 'httponly': True, } diff --git a/dokomoforms/options.py b/dokomoforms/options.py index 5d72f33b..ae0bfcc2 100644 --- a/dokomoforms/options.py +++ b/dokomoforms/options.py @@ -27,6 +27,8 @@ define('https', default=True, help=https_help, type=bool) define('organization', help='the name of your organization') +define('admin_email', help='the e-mail address of the main administrator') +define('admin_name', help='the user name of the main administrator') persona_help = ( 'the URL for login verification. Do not change this without a good reason.' diff --git a/local_config.py.example b/local_config.py.example index 2348e789..6776c4ec 100644 --- a/local_config.py.example +++ b/local_config.py.example @@ -14,6 +14,23 @@ import os ####################################### +# admin_email = "Initial administrator's e-mail address" + +# Default: 'admin@example.com' +# Set admin_email to the e-mail address of the administrator of this +# installation of Dokomo Forms. This will be the only account that can log in +# at first. + +####################################### + +# admin_name = "Initial administrator's user name" + +# Default: 'admin' +# Set admin_name to the user name of the administrator of this +# installation of Dokomo Forms. + +####################################### + # demo = True # Default: False From 00d04b05c2538f5f4cfc527abde1e0014df2450c Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Mon, 14 Dec 2015 16:54:45 -0500 Subject: [PATCH 61/69] Don't need this --- cli.ini | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 cli.ini diff --git a/cli.ini b/cli.ini deleted file mode 100644 index a7546354..00000000 --- a/cli.ini +++ /dev/null @@ -1,31 +0,0 @@ -# This is an example of the kind of things you can do in a configuration file. -# All flags used by the client can be configured here. Run Let's Encrypt with -# "--help" to learn more about the available options. - -# Use a 4096 bit RSA key instead of 2048 -# rsa-key-size = 4096 - -# Always use the staging/testing server -# server = https://acme-staging.api.letsencrypt.org/directory - -# Uncomment and update to register with the specified e-mail address -email = email@example.com - -# Uncomment and update to generate certificates for the specified -# domains. -domains = www.example.com - -# Uncomment to use a text interface instead of ncurses -# text = True - -# Uncomment to use the standalone authenticator on port 443 -# authenticator = standalone -# standalone-supported-challenges = tls-sni-01 - -# Uncomment to use the webroot authenticator. Replace webroot-path with the -# path to the public_html / webroot folder being served by your web server. -authenticator = webroot -webroot-path = /tmp/letsencrypt-auto - -agree-tos = True -renew = True From dd84caa775215c65bad3b30e13acab62143b0603 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 15 Dec 2015 09:16:59 -0500 Subject: [PATCH 62/69] Minor test restructuring --- tests/python/test_selenium.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 084cf80f..04b654cd 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -4399,7 +4399,7 @@ def setUp(self): 'http://localhost:9999/debug/toggle_revisit_slow?state=true' ) except urllib.error.URLError: - pass + self.fail('Revisit cannot be set to slow mode') def tearDown(self): super().tearDown() @@ -4437,9 +4437,9 @@ def test_single_facility_question_loading(self): overlay = self.drv.find_elements_by_class_name('loading-overlay') finish_time = time.time() - self.assertGreater(finish_time - start_time, 2) # overlay should not be present self.assertEqual(len(overlay), 0) + self.assertGreater(finish_time - start_time, 2) @report_success_status def test_facilities_only_fetched_on_first_load(self): @@ -4461,9 +4461,9 @@ def test_facilities_only_fetched_on_first_load(self): overlay = self.drv.find_elements_by_class_name('loading-overlay') finish_time = time.time() - self.assertGreater(finish_time - start_time, 2) # overlay should not be present self.assertEqual(len(overlay), 0) + self.assertGreater(finish_time - start_time, 2) # second load, should be fast because revisit is not hit self.drv.refresh() From de260e48228c52b3dfeba68c13d5b3d86195fcc3 Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 15 Dec 2015 09:43:38 -0500 Subject: [PATCH 63/69] Minor test restructuring --- tests/python/test_selenium.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 04b654cd..41e406ad 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -660,6 +660,7 @@ def test_update_settings(self): self.sleep() self.click(self.drv.find_element_by_class_name('nav-settings')) + self.sleep() self.wait_for_element('user-name') name_field = self.drv.find_element_by_id('user-name') self.click(name_field) From cce05233c7a4a36780ed49030a485a843c1c694c Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 15 Dec 2015 14:13:09 -0500 Subject: [PATCH 64/69] Installer should fail on error --- installer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.sh b/installer.sh index 7769058d..d328357c 100755 --- a/installer.sh +++ b/installer.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh # Dokomo Forms installer for version 0.2.2 -set -o errexit +set -e # Do you have docker installed? if ! command -v docker > /dev/null; then From 117087d4678d5c46018943d2dd836dde77f5e4aa Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 15 Dec 2015 14:28:21 -0500 Subject: [PATCH 65/69] :Z makes docker volumes happy http://www.projectatomic.io/blog/2015/06/using-volumes-with-docker-can-cause-problems-with-selinux/ --- installer.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/installer.sh b/installer.sh index d328357c..5a781e20 100755 --- a/installer.sh +++ b/installer.sh @@ -88,8 +88,9 @@ printf "you have set up the DNS records for your\n" printf "domain to point to this machine. \n" printf "========================================\n" $SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ - -v "/etc/letsencrypt:/etc/letsencrypt" \ - -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ + -v "/etc/letsencrypt:/etc/letsencrypt:Z" \ + -v "/var/lib/letsencrypt:/var/lib/letsencrypt:Z" \ + -v "/var/log/letsencrypt:/var/log/letsencrypt:Z" \ quay.io/letsencrypt/letsencrypt:latest auth $DOMAIN_ARGS # Run openssl dhparam @@ -145,7 +146,7 @@ printf "========================================\n" printf "Adding monthly cron job to renew SSL \n" printf "certificate. \n" printf "========================================\n" -CRON_CMD="mkdir -p /tmp/letsencrypt-auto && docker run -it --rm --name letsencrypt -v /etc/letsencrypt:/etc/letsencrypt -v /var/lib/letsencrypt:/var/lib/letsencrypt -v /tmp/letsencrypt-auto:/tmp/letsencrypt-auto -v /var/log/letsencrypt:/var/log/letsencrypt quay.io/letsencrypt/letsencrypt --renew certonly -a webroot -w /tmp/letsencrypt-auto $DOMAIN_ARGS && docker restart $USER_nginx_1" +CRON_CMD="mkdir -p /tmp/letsencrypt-auto && docker run -it --rm --name letsencrypt -v /etc/letsencrypt:/etc/letsencrypt:Z -v /var/lib/letsencrypt:/var/lib/letsencrypt:Z -v /tmp/letsencrypt-auto:/tmp/letsencrypt-auto:Z -v /var/log/letsencrypt:/var/log/letsencrypt:Z quay.io/letsencrypt/letsencrypt --renew certonly -a webroot -w /tmp/letsencrypt-auto $DOMAIN_ARGS && docker restart $USER_nginx_1" CRON_JOB="0 0 1 * * $CRON_CMD" ( crontab -l | fgrep -i -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab - From 9421cc924b9c2df88ad407b925285d410d61317b Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 15 Dec 2015 14:31:40 -0500 Subject: [PATCH 66/69] Fix log directory --- installer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.sh b/installer.sh index 5a781e20..2cbd6d20 100755 --- a/installer.sh +++ b/installer.sh @@ -90,7 +90,7 @@ printf "========================================\n" $SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt:Z" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt:Z" \ - -v "/var/log/letsencrypt:/var/log/letsencrypt:Z" \ + -v "/var/log:/var/log:Z" \ quay.io/letsencrypt/letsencrypt:latest auth $DOMAIN_ARGS # Run openssl dhparam From 3f57fd598193a10c9a669b6c13b7134c86a8a88b Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Tue, 15 Dec 2015 14:44:07 -0500 Subject: [PATCH 67/69] Improve legibility --- installer.sh | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/installer.sh b/installer.sh index 2cbd6d20..05476666 100755 --- a/installer.sh +++ b/installer.sh @@ -34,8 +34,8 @@ else DOCKER_COMPOSE=./docker-compose if ! [ -f ./docker-compose ]; then printf "========================================\n" - printf "Installing docker-compose in this \n" - printf "directory \n" + printf " Installing docker-compose in this \n" + printf " directory \n" printf "========================================\n" $CURL -o docker-compose -L https://github.com/docker/compose/releases/download/1.5.2/run.sh chmod +x docker-compose @@ -67,14 +67,14 @@ fi # Ask for domain(s) printf "========================================\n" -printf "Please enter your domain name(s) (space \n" -printf "separated) \n" +printf " Please enter your domain name(s) (space\n" +printf " separated) \n" printf " \n" -printf "Hint: for www include both \n" -printf ">>> www.your.domain your.domain \n" +printf " Hint: for www include both \n" +printf " >>> www.your.domain your.domain \n" printf " \n" -printf "For a subdomain just give \n" -printf ">>> subdomain.your.domain \n" +printf " For a subdomain just give \n" +printf " >>> subdomain.your.domain \n" printf "========================================\n" printf "Domain(s):\n>>> " read DOMAINS @@ -83,9 +83,9 @@ DOMAIN_ARGS=$(echo $DOMAINS | sed -r s/\([^\ ]+\)/-d\ \\1/g) # Run letsencrypt printf "========================================\n" -printf "Installing SSL certificate. Make sure \n" -printf "you have set up the DNS records for your\n" -printf "domain to point to this machine. \n" +printf " Installing SSL certificate. Make sure \n" +printf " you have set up the DNS records for \n" +printf " your domain to point to this machine. \n" printf "========================================\n" $SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt:Z" \ @@ -95,21 +95,21 @@ $SUDO docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ # Run openssl dhparam printf "========================================\n" -printf "Generating Diffie-Hellman parameters \n" -printf "using OpenSSL (2048 bit prime) \n" +printf " Generating Diffie-Hellman parameters \n" +printf " using OpenSSL (2048 bit prime) \n" printf "========================================\n" $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 2048 # Download the configuration files printf "========================================\n" -printf "Downloading configuration files \n" +printf " Downloading configuration files \n" printf "========================================\n" $CURL -O https://raw.githubusercontent.com/SEL-Columbia/dokomoforms/v0.2.2/docker-compose.yml $CURL -O https://raw.githubusercontent.com/SEL-Columbia/dokomoforms/v0.2.2/nginx.conf # Edit the configuration files printf "========================================\n" -printf "Generating final configuration \n" +printf " Generating final configuration \n" printf "========================================\n" touch local_config.py @@ -143,8 +143,8 @@ printf "admin_name = '${ADMIN_NAME:-$DEFAULT_NAME}'\n" >> local_config.py # Let's Encrypt auto-renew (for now this is a cron job). printf "========================================\n" -printf "Adding monthly cron job to renew SSL \n" -printf "certificate. \n" +printf " Adding monthly cron job to renew SSL \n" +printf " certificate. \n" printf "========================================\n" CRON_CMD="mkdir -p /tmp/letsencrypt-auto && docker run -it --rm --name letsencrypt -v /etc/letsencrypt:/etc/letsencrypt:Z -v /var/lib/letsencrypt:/var/lib/letsencrypt:Z -v /tmp/letsencrypt-auto:/tmp/letsencrypt-auto:Z -v /var/log/letsencrypt:/var/log/letsencrypt:Z quay.io/letsencrypt/letsencrypt --renew certonly -a webroot -w /tmp/letsencrypt-auto $DOMAIN_ARGS && docker restart $USER_nginx_1" CRON_JOB="0 0 1 * * $CRON_CMD" @@ -152,10 +152,10 @@ CRON_JOB="0 0 1 * * $CRON_CMD" # Bring up Dokomo Forms printf "========================================\n" -printf "Starting Dokomo Forms. \n" +printf " Starting Dokomo Forms. \n" printf " \n" -printf "You can view the status of the \n" -printf "containers by running: \n" -printf "$DOCKER_COMPOSE ps\n" +printf " You can view the status of the \n" +printf " containers by running: \n" +printf " $DOCKER_COMPOSE ps\n" printf "========================================\n" $DOCKER_COMPOSE up -d From 9b47c264b1b1509f01abe56cbaca8c6f2b3cac2b Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Wed, 16 Dec 2015 09:17:58 -0500 Subject: [PATCH 68/69] Tidying up --- docker-compose.yml | 2 +- docker-wait-for-postgres.sh | 2 +- installer.sh | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 39e3dde4..b9f23b1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ nginx: - /etc/letsencrypt:/etc/letsencrypt - /tmp:/tmp webapp: - image: "selcolumbia/dokomoforms" + image: "selcolumbia/dokomoforms:0.2.2" command: bash -c "./docker-wait-for-postgres.sh db && head -c 24 /dev/urandom > cookie_secret && python webapp.py" links: - "db:db" diff --git a/docker-wait-for-postgres.sh b/docker-wait-for-postgres.sh index 6c796cb0..b2c7c907 100755 --- a/docker-wait-for-postgres.sh +++ b/docker-wait-for-postgres.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env sh until psql --host=$1 --username=postgres -w &>/dev/null do echo "Waiting for PostgreSQL..." diff --git a/installer.sh b/installer.sh index 05476666..4a33e879 100755 --- a/installer.sh +++ b/installer.sh @@ -158,4 +158,7 @@ printf " You can view the status of the \n" printf " containers by running: \n" printf " $DOCKER_COMPOSE ps\n" printf "========================================\n" +if [ -f /etc/redhat-release ] ; then + chcon -Rt svirt_sandbox_file_t . +fi $DOCKER_COMPOSE up -d From e8dee9de8333568a3d54e57dafea91b8f46227df Mon Sep 17 00:00:00 2001 From: Viktor Roytman <vr2262@columbia.edu> Date: Wed, 16 Dec 2015 09:56:12 -0500 Subject: [PATCH 69/69] Skip tests with strange problems --- tests/python/test_selenium.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/python/test_selenium.py b/tests/python/test_selenium.py index 41e406ad..440c5c8b 100644 --- a/tests/python/test_selenium.py +++ b/tests/python/test_selenium.py @@ -4393,6 +4393,10 @@ def test_revisit_offline_entirely(self): class TestEnumerateSlowRevisit(DriverTest): def setUp(self): + is_travis = os.environ.get('TRAVIS', 'f').startswith('t') + if is_travis and not SAUCE_CONNECT: + raise unittest.SkipTest("These just don't work reliably on Travis") + super().setUp() try: