========================================================= JavaScript Manage UI Components, Libraries and Unit Tests ========================================================= Unit testing raises the need to organize code into separate components. Other reasons include code re-use and better management of various JavaScript libraries. In this sample we will re-write the :doc:`code_react_manage_the_list_of_categories` sample in a commonly recommended way that allows for code re-use, unit test, deployment and automatic management of third-party JavaScript packages together with their specific versions. Folders and Tools Preparation ----------------------------- We will use `npm `__ as the central place to manage the libraries, run the web server and unit tests. #. npm is packaged inside Node.js so firstly we need to download Node installer from https://nodejs.org. #. .. figure:: /_static/images/Node.js_Setup_Including_npm.png :align: right :width: 306px Install npm Run the installer and include npm in the setup. |br| #. Test that npm is installed successfully by running ``npm --version`` in the command-line to see the version number. #. Create a folder named "ui\_react\_samples\_2" anywhere available. #. Inside it, create a folder named "src". This is where we will put our code, with each component in a separate file. Because of this structure, we now must use a web server even for testing. For details, please see `same-origin policy `__. #. Inside "src" folder, create a folder named "\_\_tests\_\_". This is where we will put the unit tests. #. In command-line, go to "ui\_react\_samples\_2" folder and run ``npm init`` to create npm configuration file "package.json". #. Accept default values by pressing Enter. #. The content of "ui\_react\_samples\_2/package.json" will be as following: .. code-block:: json { "name": "ui_react_samples_2", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } Install the Packages -------------------- We need the following packages: - In both production and development: - `react `__ and `react-dom `__ for the UI. - `babel-loader `__ `babel-core `__ `babel-preset-es2015 `__ `babel-preset-react `__ to transpile JSX. - `request `__ for REST API calls (to replace JQuery that `needs tweaking `__ to work in Node.js). - `webpack `__ to bundle the source files together - `json-loader `__ for webpack. - In development only: - `webpack-dev-server `__ to serve the bundle. - `http-server `__ to serve HTML files. - `react-addons-test-utils `__ for unit testing. - `mocha `__ for the test framework. - `karma `__ `karma-cli `__, `karma-mocha `__, `karma-webpack `__, `karma-chrome-launcher `__ for the test runner. - `karma-sinon `__ to stub out API calls in unit tests. - `expect `__ to write assertions in unit tests. Install the packages and save to "package.json": #. In command-line, go to "ui\_react\_samples\_2" folder. #. ``npm install react react-dom --save`` #. ``npm install babel-loader babel-core babel-preset-es2015 babel-preset-react --save`` #. ``npm install request@2.65.0 --save`` (Use request at 2.65.0 because higher versions are having an issue with webpack.) #. ``npm install webpack json-loader --save`` #. ``npm install webpack webpack-dev-server http-server react-addons-test-utils mocha`` |br| ``karma karma-cli karma-mocha karma-webpack karma-chrome-launcher karma-sinon expect --save-dev`` (Packages only needed for development use ``--save-dev`` so that they are excluded when deployed for production) Configure Unit Test ------------------- #. Create a text file named "tests.webpack.js" in "ui\_react\_samples\_2" folder. .. code-block:: javascript var context = require.context('./src', true, /-test\.jsx$/); context.keys().forEach(context); The code tells webpack to bundle all files ending in ``-test.jsx`` (test cases) below "ui\_react\_samples\_2/src" folder into this "tests.webpack.js" file. #. Create a text file named "karma.conf.js" in "ui\_react\_samples\_2" folder. .. code-block:: javascript var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], client: { captureConsole: true }, singleRun: true, frameworks: ['mocha','sinon'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx$/, loader: 'babel-loader', query: {presets: ['react', 'es2015']}}, {test: /\.json$/, loader: 'json-loader'} ] }, watch: true, node: { console: true, fs: 'empty', net: 'empty', tls: 'empty' } }, webpackServer: { noInfo: true } }); }; ``files`` section says that ``tests.webpack.js`` should be used to test, and ``preprocessors`` section tells webpack to process this file in advance (which bundles all test cases into the file). Also, the ``loaders`` section inside ``webpack`` tells webpack to use babel loader for JSX and json loader for json files. #. Finally, edit "package.json" to change the npm test command to call karma: .. comment: highlight as text since Pygments cannot parse partial json .. code-block:: text "scripts": { "test": "karma start" }, Implement an Empty Component ---------------------------- Create a text file named "CategoryList.jsx" in "ui\_react\_samples\_2/src". .. comment: highlight as text since Pygments cannot parse jsx .. code-block:: text var React = require('react'); module.exports = React.createClass({ render: function() { return
} }); Write the Unit Test ------------------- #. Create a text file named "CategoryList-test.jsx" in "ui\_react\_samples\_2/src/\_\_tests\_\_". .. code-block:: javascript :linenos: :emphasize-lines: 24,25,29,30 var React = require('react'); var TestUtils = require('react-addons-test-utils'); var expect = require('expect'); var request = require('request'); var CategoryList = require('../CategoryList.jsx'); describe('CategoryList', function () { before(function(done){ sinon .stub(request, 'get') .yields( null, {statusCode:200}, JSON.stringify([ {id:"192a433a-383e-4093-a21c-266b9a3031c2", name:"Category_1"}, {id:"3c55a8ac-5763-4dfd-9e1e-788e0e741400", name:"Category_2"}, {id:"3c55a8ac-5763-4dfd-9e1e-788e0e741401", name:"Category_3"}])); done(); }); it("renders an ul with lis", function () { var categoryList = TestUtils.renderIntoDocument(); var ul = TestUtils.findRenderedDOMComponentWithTag( categoryList, 'ul' ); expect(ul).toExist(); var lis = TestUtils.scryRenderedDOMComponentsWithTag( categoryList, 'li' ); expect(lis.length).toBe(3); }); after(function(done){ request.get.restore(); done(); }); }); #. In ``before``, we use sinon to stub any GET request to return successfully by statusCode 200 with a mock array of 3 categories as response. (See :ref:`GET_advancedSetting/category/(tenant\_id)` for sample of actual response.) #. In ``"renders an ul with lis"``, we use React TestUtils to render the component with a dummy url, then use TestUtils functions to find and assert the UI elements. #. In ``after``, we restore the original request. Run the Unit Test ----------------- #. In command-line, go to "ui\_react\_samples\_2" folder and run ``npm run --silent test``. #. ``karma start`` should be called and after a while, open a Chrome browser window, then fail as expected with the messages ``CategoryList renders an ul with lis FAILED`` and ``Executed 1 of 1 (1 FAILED) ERROR``. In next sections we will work on CategoryList code to make the unit test succeed, as well as more tests to make use of React TestUtils' ``Simulate``. Run the Component in Server --------------------------- To run the component, we render it in a starting page, then use webpack to bundle the page together with the libraries, then use webpack-dev-server to serve the JavaScript bundle. This bundle will be included in an HTML page served by the http-server. #. Create the starting page named "index.jsx" in "ui\_react\_samples\_2" folder. .. code-block:: javascript var ReactDOM = require('react-dom'); var CategoryList = require('./src/CategoryList'); ReactDOM.render(, document.getElementById('app')); #. Create the default webpack configuration file named "webpack.config.js" in "ui\_react\_samples\_2" folder. .. code-block:: javascript module.exports = { entry: './index.jsx', output: { filename: 'bundle.js', publicPath: 'http://localhost:8090/assets' // url to include the bundle in HTML page will be http://localhost:8090/assets/bundle.js }, module: { loaders: [ {test: /\.jsx$/, loader: 'babel-loader', query: {presets: ['react', 'es2015']}}, {test: /\.json$/, loader: 'json'} ] }, externals: { //don't bundle the 'react' npm package with our bundle.js //but get it from a global 'React' variable 'react': 'React' }, resolve: { extensions: ['', '.js', '.jsx'] }, node: { console: true, fs: 'empty', net: 'empty', tls: 'empty' } } #. Create the HTML page named "index.html" in "ui\_react\_samples\_2" folder. .. code-block:: html
#. Add commands to "package.json" to start the server .. comment: highlight as text since Pygments cannot parse partial json .. code-block:: text "scripts": { "test": "karma start", "start": "npm run serve | npm run dev", "serve": "./node_modules/.bin/http-server -p 8080", "dev": "webpack-dev-server --progress --colors --port 8090" }, #. Start the server by running ``npm run start``. #. Open browser and go to http://localhost:8080/ to see a blank page. Implement CategoryList Component -------------------------------- The JavaScript code is nearly the same as :doc:`code_react_manage_the_list_of_categories`, but without the HTML code. .. comment: highlight as text since Pygments cannot parse jsx .. code-block:: text var React = require('react'); var request = require('request'); module.exports = React.createClass({ getInitialState: function() { return { categoryArray: new Array() }; }, componentDidMount: function() { this.fetchData(); }, render: function() { var lines = this.state.categoryArray.map(function(category) { return (
  •  
  • ) }.bind(this)); return (
      {lines}
    ) }, fetchData: function() { // TODO }, reflectChangedData: function(id, event) { var categories = this.state.categoryArray; var pos = categories.map(function(x) {return x.id; }).indexOf(id); categories[pos].name = event.target.value; this.setState({categoryArray : categories}); }, pushData: function() { // TODO }, addCategory: function(e) { e.preventDefault(); var categories = this.state.categoryArray; var newId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); categories.push({id: newId, name: null}); this.setState({categoryArray : categories}); }, removeCategory: function(id, event) { // TODO } }); The fetchData, pushData and removeCategory functions are now implemented using `request `__ library: .. code-block:: javascript fetchData: function() { request.get( { url: this.props.rootUrl + "advancedSetting/category/", headers: {"Content-Type": "application/json"}, withCredentials: false }, function (err, res, body) { if (!err && res.statusCode === 200) { this.setState({ categoryArray: JSON.parse(body) }); } else { console.log("Error in fetchData: " + JSON.stringify(res)); } }.bind(this) ); }, pushData: function() { request.post( { url: this.props.rootUrl + "advancedSetting/category/", headers: {"Content-Type": "application/json"}, withCredentials: false, body: JSON.stringify(this.state.categoryArray) }, function (err, res, body) { var parsed_body = JSON.parse(body); if (!err && res.statusCode === 200) { if (!parsed_body.success) { console.log(JSON.stringify(res)); } this.fetchData(); } else { console.log(JSON.stringify(res)); this.fetchData(); } }.bind(this) ); }, removeCategory: function(id, event) { request.delete( { url: this.props.rootUrl + "advancedSetting/category/" + id, headers: {"Content-Type": "application/json"}, withCredentials: false }, function (err, res, body) { var parsed_body = JSON.parse(body); if (!err && res.statusCode === 200) { if (!parsed_body.success) { console.log(JSON.stringify(res)); } this.fetchData(); } else { console.log(JSON.stringify(res)); this.fetchData(); } }.bind(this) ); } The page at http://localhost:8080/ will automatically be refreshed each time the code is updated. Test Behaviors with React TestUtils ----------------------------------- React TestUtils allow us to simulate mouse and keyboard events, therefore provides a way to test the event handler code. Here we will test that a new
  • tag is inserted when the Add button is clicked. Between the functions ``"renders an ul with lis"`` and ``after``, add the following function: .. code-block:: javascript :linenos: :emphasize-lines: 16 it("renders one more li when Add button is clicked", function () { var categoryList = TestUtils.renderIntoDocument(); var ul = TestUtils.findRenderedDOMComponentWithTag( categoryList, 'ul' ); expect(ul).toExist(); var buttons = TestUtils.scryRenderedDOMComponentsWithTag( categoryList, 'button' ); var addButton = buttons[0]; TestUtils.Simulate.click(addButton); var lis = TestUtils.scryRenderedDOMComponentsWithTag( categoryList, 'li' ); expect(lis.length).toBe(4); });