diff --git a/.eslintrc b/.eslintrc index 203d3501..fc16e217 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,7 +17,8 @@ "env": { "browser": true, "node": true, - "es6": true + "es6": true, + "jest": true }, "rules": { "quotes": ["error", "single"], diff --git a/.gitignore b/.gitignore index 546b06fa..bc2f2b08 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ public .cache src/components/svg plugins/gatsby-redirect-from +coverage diff --git a/.travis.yml b/.travis.yml index c87312df..0e71dafe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,19 @@ before_install: - sudo apt-get install python3-pip - sudo -H pip3 install --upgrade pip +before_script: + # https://docs.codeclimate.com/docs/travis-ci-test-coverage + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build + script: - npm test - travis_wait 60 npm run build +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT + after_success: - pip install --user awscli - export PATH=$PATH:$HOME/.local/bin diff --git a/README.md b/README.md index c9c3a276..38c80250 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@

+

@@ -28,6 +29,7 @@ - [🍬 Typekit component](#-typekit-component) - [✨ Development](#-development) - [🔮 Linting](#-linting) + - [👩‍🔬 Testing](#-testing) - [🎈 Add a new post](#-add-a-new-post) - [🚚 Deployment](#-deployment) - [🏛 Licenses](#-licenses) @@ -139,7 +141,6 @@ All SVG assets under `src/images/` will be converted to React components with th ```jsx import { ReactComponent as Logo } from './components/svg/Logo' - ; ``` @@ -185,6 +186,24 @@ npm run format npm run format:css ``` +### 👩‍🔬 Testing + +Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library). + +To run all tests, including all linting tests: + +```bash +npm test +``` + +All test files live beside the respective component. Testing setup, fixtures, and mocks can be found in `./jest.config.js` and `./jest` folder. + +For local development, run the test watcher: + +```bash +npm run test:watch +``` + ### 🎈 Add a new post ```bash diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..4c3eabea --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + transform: { + '^.+\\.jsx?$': '/jest/jest-preprocess.js' + }, + moduleNameMapper: { + '.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy', + '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/jest/__mocks__/file-mock.js', + '\\.svg': '/jest/__mocks__/svgr-mock.js' + }, + testPathIgnorePatterns: ['node_modules', '.cache', 'public', 'coverage'], + transformIgnorePatterns: ['node_modules/(?!(gatsby)/)'], + globals: { + __PATH_PREFIX__: '' + }, + testURL: 'http://localhost', + setupFiles: ['/jest/loadershim.js'], + setupFilesAfterEnv: ['/jest/setup-test-env.js'] +} diff --git a/jest/__mocks__/file-mock.js b/jest/__mocks__/file-mock.js new file mode 100644 index 00000000..0e56c5b5 --- /dev/null +++ b/jest/__mocks__/file-mock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/jest/__mocks__/gatsby.js b/jest/__mocks__/gatsby.js new file mode 100644 index 00000000..d325f6ea --- /dev/null +++ b/jest/__mocks__/gatsby.js @@ -0,0 +1,28 @@ +const React = require('react') +const gatsby = jest.requireActual('gatsby') + +module.exports = { + ...gatsby, + graphql: jest.fn(), + Link: jest.fn().mockImplementation( + // these props are invalid for an `a` tag + ({ + /* eslint-disable no-unused-vars */ + activeClassName, + activeStyle, + getProps, + innerRef, + ref, + replace, + to, + /* eslint-enable no-unused-vars */ + ...rest + }) => + React.createElement('a', { + ...rest, + href: to + }) + ), + StaticQuery: jest.fn(), + useStaticQuery: jest.fn() +} diff --git a/jest/__mocks__/svgr-mock.js b/jest/__mocks__/svgr-mock.js new file mode 100644 index 00000000..d067fa02 --- /dev/null +++ b/jest/__mocks__/svgr-mock.js @@ -0,0 +1 @@ +module.exports = { ReactComponent: 'svg' } diff --git a/jest/jest-preprocess.js b/jest/jest-preprocess.js new file mode 100644 index 00000000..95114e53 --- /dev/null +++ b/jest/jest-preprocess.js @@ -0,0 +1,5 @@ +const babelOptions = { + presets: ['babel-preset-gatsby'] +} + +module.exports = require('babel-jest').createTransformer(babelOptions) diff --git a/jest/loadershim.js b/jest/loadershim.js new file mode 100644 index 00000000..772dcc44 --- /dev/null +++ b/jest/loadershim.js @@ -0,0 +1,3 @@ +global.___loader = { + enqueue: jest.fn() +} diff --git a/jest/setup-test-env.js b/jest/setup-test-env.js new file mode 100644 index 00000000..99c14cda --- /dev/null +++ b/jest/setup-test-env.js @@ -0,0 +1,4 @@ +import 'jest-dom/extend-expect' + +// this is basically: afterEach(cleanup) +import 'react-testing-library/cleanup-after-each' diff --git a/jest/testRender.js b/jest/testRender.js new file mode 100644 index 00000000..641cc90f --- /dev/null +++ b/jest/testRender.js @@ -0,0 +1,11 @@ +import { render } from 'react-testing-library' + +const testRender = component => { + it('renders without crashing', () => { + const { container } = render(component) + + expect(container.firstChild).toBeInTheDocument() + }) +} + +export default testRender diff --git a/package.json b/package.json index 1f9447e8..eba55022 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git}/**/*'", "lint:yaml": "prettier '**/*.{yml,yaml}' --list-different", "lint": "run-p --continue-on-error lint:js lint:css lint:yaml lint:md", - "test": "npm run lint", + "test": "npm run lint && jest --coverage", + "test:watch": "npm run lint && jest --coverage --watch", "deploy": "./scripts/deploy.sh", "new": "babel-node ./scripts/new.js" }, @@ -33,36 +34,36 @@ "dms2dec": "^1.1.0", "fast-exif": "^1.0.1", "fraction.js": "^4.0.12", - "gatsby": "^2.3.22", - "gatsby-image": "^2.0.38", + "gatsby": "^2.4.2", + "gatsby-image": "^2.0.41", "gatsby-plugin-catch-links": "^2.0.13", - "gatsby-plugin-favicon": "^3.1.5", - "gatsby-plugin-feed": "^2.1.0", - "gatsby-plugin-lunr": "^1.4.0", + "gatsby-plugin-favicon": "^3.1.6", + "gatsby-plugin-feed": "^2.2.0", + "gatsby-plugin-lunr": "^1.5.0", "gatsby-plugin-matomo": "^0.7.0", "gatsby-plugin-meta-redirect": "^1.1.1", - "gatsby-plugin-offline": "^2.0.25", - "gatsby-plugin-react-helmet": "^3.0.11", + "gatsby-plugin-offline": "^2.1.0", + "gatsby-plugin-react-helmet": "^3.0.12", "gatsby-plugin-sass": "^2.0.11", - "gatsby-plugin-sharp": "^2.0.34", - "gatsby-plugin-sitemap": "^2.0.12", + "gatsby-plugin-sharp": "^2.0.36", + "gatsby-plugin-sitemap": "^2.1.0", "gatsby-plugin-svgr": "^2.0.2", "gatsby-plugin-webpack-size": "^0.0.3", "gatsby-redirect-from": "^0.1.1", "gatsby-remark-autolink-headers": "^2.0.16", - "gatsby-remark-copy-linked-files": "^2.0.11", + "gatsby-remark-copy-linked-files": "^2.0.12", "gatsby-remark-highlights": "^1.3.4", - "gatsby-remark-images": "^3.0.10", + "gatsby-remark-images": "^3.0.11", "gatsby-remark-smartypants": "^2.0.9", - "gatsby-source-filesystem": "^2.0.29", + "gatsby-source-filesystem": "^2.0.33", "gatsby-source-graphql": "^2.0.18", - "gatsby-transformer-remark": "^2.3.8", - "gatsby-transformer-sharp": "^2.1.18", - "graphql": "^14.2.0", + "gatsby-transformer-remark": "^2.3.12", + "gatsby-transformer-sharp": "^2.1.19", + "graphql": "^14.2.1", "intersection-observer": "^0.6.0", "js-scrypt": "^0.2.0", "load-script": "^1.0.0", - "node-sass": "^4.11.0", + "node-sass": "^4.12.0", "nord": "^0.2.1", "pigeon-maps": "^0.12.1", "pigeon-marker": "^0.3.4", @@ -70,7 +71,7 @@ "react-blockies": "^1.4.1", "react-clipboard.js": "^2.0.7", "react-dom": "^16.8.6", - "react-helmet": "^5.2.0", + "react-helmet": "^5.2.1", "react-modal": "^3.8.1", "react-pose": "^4.0.8", "react-qr-svg": "^2.2.1", @@ -80,31 +81,36 @@ "remark-react": "^5.0.1", "slugify": "^1.3.4", "terser": "^3.17.0", - "web3": "^1.0.0-beta.51" + "web3": "^1.0.0-beta.54" }, "devDependencies": { "@babel/node": "^7.2.2", - "@babel/preset-env": "^7.4.2", + "@babel/preset-env": "^7.4.4", "@svgr/webpack": "^4.2.0", "babel-eslint": "^10.0.1", + "babel-jest": "^24.7.1", "eslint": "^5.16.0", - "eslint-config-prettier": "^4.1.0", + "eslint-config-prettier": "^4.2.0", "eslint-loader": "^2.1.2", "eslint-plugin-graphql": "^3.0.3", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-prettier": "^3.0.1", - "eslint-plugin-react": "^7.12.4", + "eslint-plugin-react": "^7.13.0", "fs-extra": "^7.0.1", + "identity-obj-proxy": "^3.0.0", + "jest": "^24.7.1", + "jest-dom": "^3.1.4", "markdownlint-cli": "^0.15.0", "npm-run-all": "^4.1.5", - "ora": "^3.2.0", + "ora": "^3.4.0", "pify": "^4.0.1", "prettier": "^1.17.0", "prettier-stylelint": "^0.4.2", - "stylelint": "^10.0.0", - "stylelint-config-css-modules": "^1.3.0", + "react-testing-library": "^7.0.0", + "stylelint": "^10.0.1", + "stylelint-config-css-modules": "^1.4.0", "stylelint-config-standard": "^18.3.0", - "stylelint-scss": "^3.5.4", + "stylelint-scss": "^3.6.1", "why-did-you-update": "^1.0.6" }, "engines": { diff --git a/src/components/atoms/Container.test.jsx b/src/components/atoms/Container.test.jsx new file mode 100644 index 00000000..370ce8af --- /dev/null +++ b/src/components/atoms/Container.test.jsx @@ -0,0 +1,9 @@ +import React from 'react' +// import { render } from 'react-testing-library' +import testRender from '../../../jest/testRender' + +import Container from './Container' + +describe('Container', () => { + testRender(Hello) +}) diff --git a/src/components/atoms/Exif.jsx b/src/components/atoms/Exif.jsx index c805f3b3..ee788675 100644 --- a/src/components/atoms/Exif.jsx +++ b/src/components/atoms/Exif.jsx @@ -1,70 +1,8 @@ -import React, { Fragment, PureComponent } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import Map from 'pigeon-maps' -import Marker from 'pigeon-marker' +import ExifMap from './ExifMap' import styles from './Exif.module.scss' -const MAPBOX_ACCESS_TOKEN = - 'pk.eyJ1Ijoia3JlbWFsaWNpb3VzIiwiYSI6ImNqbTE2NHpkYjJmNm8zcHF4eDVqZzk3ejEifQ.1uwPzM6MSTgL2e1Hxcmuqw' - -const retina = - typeof window !== 'undefined' && window.devicePixelRatio >= 2 ? '@2x' : '' - -const mapbox = (mapboxId, accessToken) => (x, y, z) => - `https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${retina}?access_token=${accessToken}` - -const providers = { - osm: (x, y, z) => { - const s = String.fromCharCode(97 + ((x + y + z) % 3)) - return `https://${s}.tile.openstreetmap.org/${z}/${x}/${y}.png` - }, - wikimedia: (x, y, z) => - `https://maps.wikimedia.org/osm-intl/${z}/${x}/${y}${retina}.png`, - stamen: (x, y, z) => - `https://stamen-tiles.a.ssl.fastly.net/terrain/${z}/${x}/${y}${retina}.jpg`, - streets: mapbox('streets-v10', MAPBOX_ACCESS_TOKEN), - satellite: mapbox('satellite-streets-v10', MAPBOX_ACCESS_TOKEN), - outdoors: mapbox('outdoors-v10', MAPBOX_ACCESS_TOKEN), - light: mapbox('light-v9', MAPBOX_ACCESS_TOKEN), - dark: mapbox('dark-v9', MAPBOX_ACCESS_TOKEN) -} - -class ExifMap extends PureComponent { - state = { zoom: 12 } - - static propTypes = { - gps: PropTypes.object - } - - zoomIn = () => { - this.setState({ - zoom: Math.min(this.state.zoom + 4, 20) - }) - } - - render() { - const { latitude, longitude } = this.props.gps - - return ( - - - - ) - } -} - export default class Exif extends PureComponent { static propTypes = { exif: PropTypes.object @@ -82,7 +20,7 @@ export default class Exif extends PureComponent { } = this.props.exif return ( - + <> - + ) } } diff --git a/src/components/atoms/Exif.test.jsx b/src/components/atoms/Exif.test.jsx new file mode 100644 index 00000000..b4c497af --- /dev/null +++ b/src/components/atoms/Exif.test.jsx @@ -0,0 +1,19 @@ +import React from 'react' +// import { render } from 'react-testing-library' +import testRender from '../../../jest/testRender' + +import Exif from './Exif' + +const exif = { + iso: '500', + model: 'Canon', + fstop: '7.2', + shutterspeed: '200', + focalLength: '200', + exposure: '200', + gps: { latitude: '52.4792516', longitude: '13.431609' } +} + +describe('Exif', () => { + testRender() +}) diff --git a/src/components/atoms/ExifMap.jsx b/src/components/atoms/ExifMap.jsx new file mode 100644 index 00000000..c2107739 --- /dev/null +++ b/src/components/atoms/ExifMap.jsx @@ -0,0 +1,65 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Map from 'pigeon-maps' +import Marker from 'pigeon-marker' + +const MAPBOX_ACCESS_TOKEN = + 'pk.eyJ1Ijoia3JlbWFsaWNpb3VzIiwiYSI6ImNqbTE2NHpkYjJmNm8zcHF4eDVqZzk3ejEifQ.1uwPzM6MSTgL2e1Hxcmuqw' + +const retina = + typeof window !== 'undefined' && window.devicePixelRatio >= 2 ? '@2x' : '' + +const mapbox = (mapboxId, accessToken) => (x, y, z) => + `https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${retina}?access_token=${accessToken}` + +const providers = { + // osm: (x, y, z) => { + // const s = String.fromCharCode(97 + ((x + y + z) % 3)) + // return `https://${s}.tile.openstreetmap.org/${z}/${x}/${y}.png` + // }, + // wikimedia: (x, y, z) => + // `https://maps.wikimedia.org/osm-intl/${z}/${x}/${y}${retina}.png`, + // stamen: (x, y, z) => + // `https://stamen-tiles.a.ssl.fastly.net/terrain/${z}/${x}/${y}${retina}.jpg`, + // streets: mapbox('streets-v10', MAPBOX_ACCESS_TOKEN), + // satellite: mapbox('satellite-streets-v10', MAPBOX_ACCESS_TOKEN), + // outdoors: mapbox('outdoors-v10', MAPBOX_ACCESS_TOKEN), + light: mapbox('light-v9', MAPBOX_ACCESS_TOKEN), + dark: mapbox('dark-v9', MAPBOX_ACCESS_TOKEN) +} + +export default class ExifMap extends PureComponent { + state = { zoom: 12 } + + static propTypes = { + gps: PropTypes.object + } + + zoomIn = () => { + this.setState({ + zoom: Math.min(this.state.zoom + 4, 20) + }) + } + + render() { + const { latitude, longitude } = this.props.gps + + return ( + + + + ) + } +} diff --git a/src/components/atoms/Hamburger.test.jsx b/src/components/atoms/Hamburger.test.jsx new file mode 100644 index 00000000..9b6c61c9 --- /dev/null +++ b/src/components/atoms/Hamburger.test.jsx @@ -0,0 +1,9 @@ +import React from 'react' +// import { render } from 'react-testing-library' +import testRender from '../../../jest/testRender' + +import Hamburger from './Hamburger' + +describe('Hamburger', () => { + testRender() +}) diff --git a/src/components/atoms/Input.test.jsx b/src/components/atoms/Input.test.jsx new file mode 100644 index 00000000..52126d8e --- /dev/null +++ b/src/components/atoms/Input.test.jsx @@ -0,0 +1,9 @@ +import React from 'react' +// import { render } from 'react-testing-library' +import testRender from '../../../jest/testRender' + +import Input from './Input' + +describe('Input', () => { + testRender() +})