From eb7a934bf5340ff975f3bff902047ca71c596906 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Sun, 10 Jul 2016 19:30:44 +0200 Subject: [PATCH] [feature/react-intl] merge feature/redux (#734) * Add support for helpers in redux-thunk actions (#650) Support for isomorphic `fetch` and `graphqlRequest` helpers in redux-thunk action creators * [feature/redux] Fix: Redux helpers client configuration (#663) * Update npm modules; tweak Stylelint settings * Change db string length (#691) * Update history module to v3 (#692) - Update `history` dependency to v3.0.0 ([changelog](https://github.com/ReactJSTraining/history/blob/master/CHANGES.md)) - Add `windowScrollX` and `windowScrollY` helpers to `core/DOMUtils` - Rename `match()` to `UniversalRouter.resolve()` - Fix scroll issues ([see article](https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration)) * fix(Isomorphic Style Loader): Add support to load multiple styles (#678) * fix(Isomorphic Style Loader): Add support to load multiple styles * fix(Isomorphic Style Loader): Add remove feature back * Adds testing section (#687) Integrates comments by @langpavel * Fix npm warnings about graphql dependencies (#693) fixes #661 * Fix OccurrenceOrderPlugin spelling (#683) * fixed typo in passport.js (#696) * Docs: use more expressive language (#701) * Add eslint global-require exception (#703) Add eslint global-require exception, based in 'src/server.js' file. The 'feature/react-intl' branch also has this problem. * Update stylelint-config-standard (#707) * Update stylelint-config-standard When I run 'npm run lint', this warning show up: Deprecation Warning: 'number-zero-length-no-unit' has been deprecated, and will be removed in '7.0'. Use 'length-zero-no-unit' instead. See: http://stylelint.io/user-guide/rules/length-zero-no-unit/ update package to remove it. * Update stylelint to use length-zero-no-unit instead number-zero-length-no-unit. * Fix spelling of "vice versa" (#710) * Remove jade dependency * Update react-style-guide.md (#718) replace ../Nav with ../Navigation * Update CHANGELOG.md --- CHANGELOG.md | 2 + README.md | 2 +- docs/README.md | 1 + docs/react-style-guide.md | 2 +- docs/testing-your-application.md | 97 ++++++++++++++++++++++++++++++++ package.json | 65 +++++++++++---------- src/client.js | 77 ++++++++++++++++--------- src/components/Html.js | 50 ++++++++++++++++ src/config.js | 5 +- src/content/about.jade | 47 ---------------- src/content/about.md | 46 +++++++++++++++ src/content/index.jade | 22 -------- src/content/index.md | 19 +++++++ src/content/privacy.jade | 52 ----------------- src/content/privacy.md | 46 +++++++++++++++ src/core/DOMUtils.js | 10 ++++ src/core/passport.js | 2 +- src/data/models/User.js | 2 +- src/data/models/UserProfile.js | 4 +- src/data/queries/content.js | 8 +-- src/routes/error/ErrorPage.js | 6 +- src/server.js | 57 +++++++++++-------- src/serverIntlPolyfill.js | 4 +- src/store/configureStore.js | 57 +++++++++++-------- src/store/createHelpers.js | 50 ++++++++++++++++ src/views/error.jade | 61 -------------------- src/views/index.jade | 18 ------ tools/webpack.config.js | 6 +- 28 files changed, 491 insertions(+), 327 deletions(-) create mode 100644 docs/testing-your-application.md create mode 100644 src/components/Html.js delete mode 100644 src/content/about.jade create mode 100644 src/content/about.md delete mode 100644 src/content/index.jade create mode 100644 src/content/index.md delete mode 100644 src/content/privacy.jade create mode 100644 src/content/privacy.md create mode 100644 src/store/createHelpers.js delete mode 100644 src/views/error.jade delete mode 100644 src/views/index.jade diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff9ac7f9..30f2c6f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased][unreleased] +- Remove Jade dependency in favor of React-based templates: `src/views/index.jade => src/components/Html` + (BREAKING CHANGE) [#711](https://github.com/kriasoft/react-starter-kit/pull/711) - Update `isomorphic-style-loader` to `v1.0.0`, it adds comparability with ES2015+ decorators. Code such as `export default withStyles(MyComponent, style1, style2)` must be replaced with `export default withStyles(style1, style2)(MyComponent)` (BREAKING CHANGE). diff --git a/README.md b/README.md index 40037b8ed..63fc7d824 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ branches that you can use either as a reference or merge into your project: isomorphic Redux and React Intl by [Pavel Lang](https://github.com/langpavel) (see [how to integrate React Intl](./docs/recipes/how-to-integrate-react-intl.md)) -If you think that any of these features should be on `master`, or vise versa, some features should +If you think that any of these features should be on `master`, or vice versa, some features should removed from the `master` branch, please [let us know](https://gitter.im/kriasoft/react-starter-kit). We love your feedback! diff --git a/docs/README.md b/docs/README.md index b314fe324..522107b96 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ * [React Style Guide](./react-style-guide.md) * [How to configure text editors and IDEs](./how-to-configure-text-editors.md) * [Data fetching with WHATWG Fetch](./data-fetching.md) +* [Testing your application](./testing-your-application.md) ### Questions diff --git a/docs/react-style-guide.md b/docs/react-style-guide.md index a54d2da8d..2a90eaee0 100644 --- a/docs/react-style-guide.md +++ b/docs/react-style-guide.md @@ -21,7 +21,7 @@ * Add `package.json` file into each component's folder.
This will allow to easily reference such components from other places in your code.
- `import Nav from '../Nav'` vs `import Nav from '../Nav/Nav.js'` + Use `import Nav from '../Navigation'` instead of `import Nav from '../Navigation/Navigation.js'` ``` /components/Navigation/icon.svg diff --git a/docs/testing-your-application.md b/docs/testing-your-application.md new file mode 100644 index 000000000..2cf77c74c --- /dev/null +++ b/docs/testing-your-application.md @@ -0,0 +1,97 @@ +## Testing your application + +### Used libraries + +RSK comes with the following libraries for testing purposes: + +- [Mocha](https://mochajs.org/) - Node.js and browser test runner +- [Chai](http://chaijs.com/) - Assertion library +- [Enzyme](https://github.com/airbnb/enzyme) - Testing utilities for React + +You may also want to take a look at the following related packages: + +- [jsdom](https://github.com/tmpvar/jsdom) +- [react-addons-test-utils](https://www.npmjs.com/package/react-addons-test-utils) + +### Running tests + +To test your application simply run the +[`npm test`](https://github.com/kriasoft/react-starter-kit/blob/b22b1810461cec9c53eedffe632a3ce70a6b29a3/package.json#L154) +command which will: +- recursively find all files ending with `.test.js` in your `src/` directory +- mocha execute found files + +```bash +npm test +``` + +### Conventions + +- test filenames MUST end with `test.js` or `npm test` will not be able to detect them +- test filenames SHOULD be named after the related component (e.g. create `Login.test.js` for +`Login.js` component) + +### Basic example + +To help you on your way RSK comes with the following +[basic test case](https://github.com/kriasoft/react-starter-kit/blob/master/src/components/App/App.test.js) +you can use as a starting point: + +```js +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import App from './App'; + +describe('App', () => { + + it('renders children correctly', () => { + const wrapper = shallow( + {} }}> +
+ + ); + + expect(wrapper.contains(
)).to.be.true; + }); + +}); +``` + +### React-intl example + +React-intl users MUST render/wrap components inside an IntlProvider like the example below: + +The example below example is a drop-in test for the RSK `Header` component: + +```js +import React from 'react'; +import Header from './Header'; +import IntlProvider from 'react-intl'; +import Navigation from '../../components/Navigation'; + +describe('A test suite for
', () => { + + it('should contain a component', () => { + it('rendering', () => { + const wrapper = renderIntoDocument(
); + expect(wrapper.find(Navigation)).to.have.length(1); + }); + }); + +}); +``` + +Please note that NOT using IntlProvider will produce the following error: + +> Invariant Violation: [React Intl] Could not find required `intl` object. +> needs to exist in the component ancestry. + +### Linting + +Running RSK eslint will also scan your test files: + +```bash +npm run eslint +``` + diff --git a/package.json b/package.json index f74e80803..d91d8dbe5 100644 --- a/package.json +++ b/package.json @@ -5,45 +5,44 @@ "npm": ">=3.3 <4" }, "dependencies": { - "babel-polyfill": "6.9.0", - "babel-runtime": "6.9.0", + "babel-polyfill": "6.9.1", + "babel-runtime": "6.9.2", "bluebird": "3.4.0", "body-parser": "1.15.1", "classnames": "2.2.5", - "cookie-parser": "1.4.1", + "cookie-parser": "1.4.3", "core-js": "2.4.0", "eventemitter3": "1.2.0", "express": "4.13.4", - "express-graphql": "0.5.1", + "express-graphql": "0.5.3", "express-jwt": "3.4.0", "express-request-language": "^1.1.4", "fastclick": "1.0.6", - "fbjs": "0.8.2", - "front-matter": "2.0.7", - "graphiql": "0.7.1", + "fbjs": "0.8.3", + "front-matter": "2.1.0", + "graphiql": "0.7.2", "graphql": "0.6.0", - "history": "2.1.1", + "history": "3.0.0", "intl": "^1.1.0", "intl-locales-supported": "^1.0.0", "isomorphic-style-loader": "1.0.0", - "jade": "1.11.0", - "jsonwebtoken": "6.2.0", - "markdown-it": "6.0.2", - "node-fetch": "1.5.2", + "jsonwebtoken": "7.0.0", + "markdown-it": "6.0.5", + "node-fetch": "1.5.3", "normalize.css": "4.1.1", "passport": "0.3.2", "passport-facebook": "2.1.1", "pretty-error": "2.0.0", - "react": "15.0.2", - "react-dom": "15.0.2", + "react": "15.1.0", + "react-dom": "15.1.0", "react-intl": "^2.1.0", "react-redux": "^4.4.5", "redux": "^3.4.0", - "redux-thunk": "^2.0.1", - "sequelize": "3.23.2", + "redux-thunk": "^2.1.0", + "sequelize": "3.23.3", "source-map-support": "0.4.0", "sqlite3": "3.1.4", - "universal-router": "1.2.1", + "universal-router": "1.2.2", "whatwg-fetch": "1.0.0" }, "devDependencies": { @@ -58,7 +57,7 @@ "babel-plugin-rewire": "^1.0.0-rc-3", "babel-plugin-transform-react-constant-elements": "^6.8.0", "babel-plugin-transform-react-inline-elements": "^6.8.0", - "babel-plugin-transform-react-remove-prop-types": "^0.2.6", + "babel-plugin-transform-react-remove-prop-types": "^0.2.7", "babel-plugin-transform-runtime": "^6.9.0", "babel-preset-es2015": "^6.9.0", "babel-preset-node5": "^11.1.0", @@ -75,20 +74,19 @@ "eslint": "^2.10.2", "eslint-config-airbnb": "^9.0.1", "eslint-loader": "^1.3.0", - "eslint-plugin-import": "^1.8.0", - "eslint-plugin-jsx-a11y": "^1.2.0", + "eslint-plugin-import": "^1.8.1", + "eslint-plugin-jsx-a11y": "^1.2.2", "eslint-plugin-react": "^5.1.1", "extend": "^3.0.0", "file-loader": "^0.8.5", "gaze": "^1.0.0", "git-repository": "^0.1.4", "glob": "^7.0.3", - "jade-loader": "^0.8.0", "json-loader": "^0.5.4", "mkdirp": "^0.5.1", - "mocha": "^2.4.5", + "mocha": "^2.5.3", "ncp": "^2.0.0", - "pixrem": "^3.0.0", + "pixrem": "^3.0.1", "pleeease-filters": "^3.0.0", "postcss": "^5.0.21", "postcss-calc": "^5.2.1", @@ -104,16 +102,16 @@ "postcss-selector-matches": "^2.0.1", "postcss-selector-not": "^2.0.0", "raw-loader": "^0.5.1", - "react-addons-test-utils": "^15.0.2", + "react-addons-test-utils": "^15.1.0", "react-transform-catch-errors": "^1.0.2", "react-transform-hmr": "^1.0.4", - "redbox-react": "^1.2.4", + "redbox-react": "^1.2.6", "redux-logger": "^2.6.1", "sinon": "^2.0.0-pre", - "stylelint": "^6.4.1", - "stylelint-config-standard": "^7.0.0", + "stylelint": "^6.6.0", + "stylelint-config-standard": "^9.0.0", "url-loader": "^0.5.7", - "webpack": "^1.13.0", + "webpack": "^1.13.1", "webpack-hot-middleware": "^2.10.0", "webpack-middleware": "^1.5.1" }, @@ -152,7 +150,16 @@ "stylelint": { "extends": "stylelint-config-standard", "rules": { - "string-quotes": "single" + "string-quotes": "single", + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": [ + "global", + "local" + ] + } + ] } }, "scripts": { diff --git a/src/client.js b/src/client.js index 88f6aa1d6..771200df8 100644 --- a/src/client.js +++ b/src/client.js @@ -11,11 +11,17 @@ import 'babel-polyfill'; import React from 'react'; import ReactDOM from 'react-dom'; import FastClick from 'fastclick'; -import { match } from 'universal-router'; +import UniversalRouter from 'universal-router'; import routes from './routes'; import history from './core/history'; import configureStore from './store/configureStore'; -import { addEventListener, removeEventListener } from './core/DOMUtils'; +import { readState, saveState } from 'history/lib/DOMStateStorage'; +import { + addEventListener, + removeEventListener, + windowScrollX, + windowScrollY, +} from './core/DOMUtils'; import Provide from './components/Provide'; import { addLocaleData } from 'react-intl'; @@ -27,7 +33,12 @@ import cs from 'react-intl/locale-data/cs'; const context = { store: null, - insertCss: styles => styles._insertCss(), // eslint-disable-line no-underscore-dangle + insertCss: (...styles) => { + const removeCss = styles.map(style => style._insertCss()); // eslint-disable-line no-underscore-dangle, max-len + return () => { + removeCss.forEach(f => f()); + }; + }, setTitle: value => (document.title = value), setMeta: (name, content) => { // Remove and create a new tag in order to make it work @@ -65,7 +76,9 @@ let renderComplete = (state, callback) => { // Google Analytics tracking. Don't send 'pageview' event after // the initial rendering, as it was already sent - window.ga('send', 'pageview'); + if (window.ga) { + window.ga('send', 'pageview'); + } callback(true); }; @@ -95,52 +108,60 @@ function render(container, state, config, component) { } function run() { - let currentLocation = null; const container = document.getElementById('app'); const initialState = JSON.parse( document. getElementById('source'). getAttribute('data-initial-state') ); + let currentLocation = history.getCurrentLocation(); // Make taps on links and buttons work fast on mobiles FastClick.attach(document.body); - const store = configureStore(initialState); + const store = configureStore(initialState, {}); context.store = store; // Re-render the app when window.location changes - const removeHistoryListener = history.listen(location => { + function onLocationChange(location) { + // Save the page scroll position into the current location's state + if (currentLocation.key) { + saveState(currentLocation.key, { + ...readState(currentLocation.key), + scrollX: windowScrollX(), + scrollY: windowScrollY(), + }); + } currentLocation = location; - match(routes, { + + UniversalRouter.resolve(routes, { path: location.pathname, query: location.query, state: location.state, context, - render: render.bind(undefined, container, location.state, { store }), + render: render.bind(undefined, container, location.state, { store }), // eslint-disable-line react/jsx-no-bind, max-len }).catch(err => console.error(err)); // eslint-disable-line no-console - }); + } - // Save the page scroll position into the current location's state - const supportPageOffset = window.pageXOffset !== undefined; - const isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat'); - const setPageOffset = () => { - currentLocation.state = currentLocation.state || Object.create(null); - if (supportPageOffset) { - currentLocation.state.scrollX = window.pageXOffset; - currentLocation.state.scrollY = window.pageYOffset; - } else { - currentLocation.state.scrollX = isCSS1Compat ? - document.documentElement.scrollLeft : document.body.scrollLeft; - currentLocation.state.scrollY = isCSS1Compat ? - document.documentElement.scrollTop : document.body.scrollTop; - } - }; + // Add History API listener and trigger initial change + const removeHistoryListener = history.listen(onLocationChange); + history.replace(currentLocation); - addEventListener(window, 'scroll', setPageOffset); - addEventListener(window, 'pagehide', () => { - removeEventListener(window, 'scroll', setPageOffset); + // https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration + let originalScrollRestoration; + if (window.history && 'scrollRestoration' in window.history) { + originalScrollRestoration = window.history.scrollRestoration; + window.history.scrollRestoration = 'manual'; + } + + // Prevent listeners collisions during history navigation + addEventListener(window, 'pagehide', function onPageHide() { + removeEventListener(window, 'pagehide', onPageHide); removeHistoryListener(); + if (originalScrollRestoration) { + window.history.scrollRestoration = originalScrollRestoration; + originalScrollRestoration = undefined; + } }); } diff --git a/src/components/Html.js b/src/components/Html.js new file mode 100644 index 000000000..1ce3eae3d --- /dev/null +++ b/src/components/Html.js @@ -0,0 +1,50 @@ +import React, { PropTypes } from 'react'; +import { analytics } from '../config'; + +function Html({ title, description, style, script, children, lang, state }) { + return ( + + + + + {title} + + + +