diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..2e236249 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-object-rest-spread"], + "presets": ["es2015", "react"] +} diff --git a/package.json b/package.json index 3190fdc5..ba1ba054 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "deploy": "touch playground/.nojekyll && gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"", "lint": "eslint . --ext .js,.jsx", "start": "webpack-dev-server", - "tap": "./node_modules/.bin/tap ./test/*.js", - "test": "npm run lint && npm run build && npm run tap", + "test": "npm run lint && npm run build && npm run unit", + "unit": "jest", "watch": "webpack --progress --colors --watch" }, "author": "Massachusetts Institute of Technology", @@ -28,35 +28,51 @@ "autoprefixer": "7.1.1", "babel-core": "^6.23.1", "babel-eslint": "^7.1.1", + "babel-jest": "^20.0.3", "babel-loader": "^7.0.0", "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", "classnames": "2.2.5", "css-loader": "0.28.3", + "enzyme": "^2.8.2", "eslint": "^3.16.1", + "eslint-config-import": "^0.13.0", "eslint-config-scratch": "^3.0.0", + "eslint-plugin-import": "^2.7.0", "eslint-plugin-react": "^7.0.1", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", "html-webpack-plugin": "2.28.0", + "jest": "^20.0.4", + "keymirror": "0.1.1", + "lodash.bindall": "4.4.0", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", "mkdirp": "^0.5.1", - "paper": "^0.11.4", + "paper": "0.11.4", "postcss-import": "^10.0.0", "postcss-loader": "^2.0.5", "postcss-simple-vars": "^4.0.0", "prop-types": "^15.5.10", - "react": "15.5.4", + "react": "15.6.1", "react-dom": "15.5.4", "react-intl": "2.3.0", "react-redux": "5.0.5", + "react-test-renderer": "^15.5.4", "redux": "3.6.0", + "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", + "regenerator-runtime": "^0.10.5", "rimraf": "^2.6.1", "style-loader": "^0.18.0", "tap": "^10.2.0", "webpack": "^2.4.1", "webpack-dev-server": "^2.4.1" + }, + "jest": { + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", + "\\.(css|less)$": "/test/__mocks__/styleMock.js" + } } } diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 7ff859ca..d5985f2d 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -1,7 +1,13 @@ module.exports = { root: true, - extends: ['scratch', 'scratch/es6', 'scratch/react'], + extends: ['scratch', 'scratch/es6', 'scratch/react', 'import'], env: { browser: true + }, + rules: { + 'import/no-mutable-exports': 'error', + 'import/no-commonjs': 'error', + 'import/no-amd': 'error', + 'import/no-nodejs-modules': 'error' } }; diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index c55ef2c5..1bad869e 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -3,24 +3,27 @@ import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushTool from '../containers/tools/brush-tool.jsx'; import EraserTool from '../containers/tools/eraser-tool.jsx'; +import ToolTypes from '../tools/tool-types.js'; -const PaintEditorComponent = props => ( -
- - - -
-); +class PaintEditorComponent extends React.Component { + render () { + return ( +
+ { + this.canvas = canvas; + }} + tool={this.props.tool} + /> + + +
+ ); + } +} PaintEditorComponent.propTypes = { - canvasId: PropTypes.string.isRequired, - tool: PropTypes.shape({ - name: PropTypes.string.isRequired - }) + tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired }; - -module.exports = PaintEditorComponent; +export default PaintEditorComponent; diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index c9c0bcdf..12d40e54 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -7,27 +7,25 @@ import {connect} from 'react-redux'; class PaintEditor extends React.Component { componentDidMount () { - const onKeyPress = this.props.onKeyPress; - document.onkeydown = function (e) { - e = e || window.event; - onKeyPress(e); - }; + document.addEventListener('keydown', this.props.onKeyPress); + } + componentWillUnmount () { + document.removeEventListener('keydown', this.props.onKeyPress); } render () { + const { + onKeyPress, // eslint-disable-line no-unused-vars + ...props + } = this.props; return ( - + ); } } PaintEditor.propTypes = { onKeyPress: PropTypes.func.isRequired, - tool: PropTypes.shape({ - name: PropTypes.string.isRequired - }) + tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired }; const mapStateToProps = state => ({ @@ -43,7 +41,7 @@ const mapDispatchToProps = dispatch => ({ } }); -module.exports = connect( +export default connect( mapStateToProps, mapDispatchToProps )(PaintEditor); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 5d08911d..fed3620a 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -4,13 +4,8 @@ import paper from 'paper'; import ToolTypes from '../tools/tool-types.js'; class PaperCanvas extends React.Component { - constructor (props) { - super(props); - this.state = { - }; - } componentDidMount () { - paper.setup('paper-canvas'); + paper.setup(this.canvas); // Create a Paper.js Path to draw a line into it: const path = new paper.Path(); // Give the stroke a color @@ -24,25 +19,22 @@ class PaperCanvas extends React.Component { // Draw the view now: paper.view.draw(); } - componentWillReceiveProps (nextProps) { - if (nextProps.tool !== this.props.tool && nextProps.tool instanceof ToolTypes) { - // TODO switch tool - } - } componentWillUnmount () { + paper.remove(); } render () { return ( - + { + this.canvas = canvas; + }} + /> ); } } PaperCanvas.propTypes = { - canvasId: PropTypes.string.isRequired, - tool: PropTypes.shape({ - name: PropTypes.string.isRequired - }) + tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired }; -module.exports = PaperCanvas; +export default PaperCanvas; diff --git a/src/containers/tools/brush-tool.jsx b/src/containers/tools/brush-tool.jsx index 931f2134..53bc763c 100644 --- a/src/containers/tools/brush-tool.jsx +++ b/src/containers/tools/brush-tool.jsx @@ -22,6 +22,7 @@ class BrushTool extends React.Component { } componentDidMount () { if (this.props.tool === BrushTool.TOOL_TYPE) { + debugger; this.activateTool(); } } @@ -38,8 +39,7 @@ class BrushTool extends React.Component { return false; // Logic only component } activateTool () { - document.getElementById(this.props.canvasId) - .addEventListener('mousewheel', this.onScroll); + this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); this.blob.activateTool(false /* isEraser */, this.tool, this.props.brushToolState); @@ -56,8 +56,7 @@ class BrushTool extends React.Component { this.tool.activate(); } deactivateTool () { - document.getElementById(this.props.canvasId) - .removeEventListener('mousewheel', this.onScroll); + this.props.canvas.removeEventListener('mousewheel', this.onScroll); } onScroll (event) { if (event.deltaY < 0) { @@ -69,7 +68,7 @@ class BrushTool extends React.Component { } render () { return ( -
+
Brush Tool
); } } @@ -78,7 +77,7 @@ BrushTool.propTypes = { brushToolState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), - canvasId: PropTypes.string.isRequired, + canvas: PropTypes.element, changeBrushSize: PropTypes.func.isRequired, tool: PropTypes.shape({ name: PropTypes.string.isRequired @@ -95,7 +94,7 @@ const mapDispatchToProps = dispatch => ({ } }); -module.exports = connect( +export default connect( mapStateToProps, mapDispatchToProps )(BrushTool); diff --git a/src/containers/tools/eraser-tool.jsx b/src/containers/tools/eraser-tool.jsx index c3fa1656..9ee9fa6a 100644 --- a/src/containers/tools/eraser-tool.jsx +++ b/src/containers/tools/eraser-tool.jsx @@ -38,16 +38,14 @@ class EraserTool extends React.Component { return false; // Logic only component } activateTool () { - document.getElementById(this.props.canvasId) - .addEventListener('mousewheel', this.onScroll); + this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); this.blob.activateTool(true /* isEraser */, this.tool, this.props.eraserToolState); this.tool.activate(); } deactivateTool () { - document.getElementById(this.props.canvasId) - .removeEventListener('mousewheel', this.onScroll); + this.props.canvas.removeEventListener('mousewheel', this.onScroll); this.blob.deactivateTool(); } onScroll (event) { @@ -60,13 +58,13 @@ class EraserTool extends React.Component { } render () { return ( -
+
Eraser Tool
); } } EraserTool.propTypes = { - canvasId: PropTypes.string.isRequired, + canvas: PropTypes.element, changeBrushSize: PropTypes.func.isRequired, eraserToolState: PropTypes.shape({ brushSize: PropTypes.number.isRequired @@ -86,7 +84,7 @@ const mapDispatchToProps = dispatch => ({ } }); -module.exports = connect( +export default connect( mapStateToProps, mapDispatchToProps )(EraserTool); diff --git a/src/log/log.js b/src/log/log.js index 9b991a58..91414157 100644 --- a/src/log/log.js +++ b/src/log/log.js @@ -1,4 +1,4 @@ -const minilog = require('minilog'); +import minilog from 'minilog'; minilog.enable(); -module.exports = minilog('paint-editor'); +export default minilog('scratch-paint'); diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index 31ad10cf..8a27161e 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -2,15 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PaintEditor from '..'; import {Provider} from 'react-redux'; -import {createStore, applyMiddleware} from 'redux'; -import throttle from 'redux-throttle'; +import {createStore} from 'redux'; import reducer from '../reducers/combine-reducers'; const appTarget = document.createElement('div'); document.body.appendChild(appTarget); -const store = applyMiddleware( - throttle(300, {leading: true, trailing: true}) -)(createStore)( +const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); diff --git a/src/reducers/brush-tool.js b/src/reducers/brush-tool.js index 7e59c560..0bc422ce 100644 --- a/src/reducers/brush-tool.js +++ b/src/reducers/brush-tool.js @@ -22,4 +22,4 @@ reducer.changeBrushSize = function (brushSize) { }; }; -module.exports = reducer; +export default reducer; diff --git a/src/reducers/combine-reducers.js b/src/reducers/combine-reducers.js index 75a429b1..a5654a63 100644 --- a/src/reducers/combine-reducers.js +++ b/src/reducers/combine-reducers.js @@ -1,7 +1,10 @@ import {combineReducers} from 'redux'; +import toolReducer from './tools'; +import brushToolReducer from './brush-tool'; +import eraserToolReducer from './eraser-tool'; -module.exports = combineReducers({ - tool: require('./tools'), - brushTool: require('./brush-tool'), - eraserTool: require('./eraser-tool') +export default combineReducers({ + tool: toolReducer, + brushTool: brushToolReducer, + eraserTool: eraserToolReducer }); diff --git a/src/reducers/eraser-tool.js b/src/reducers/eraser-tool.js index e8371e3e..a985e666 100644 --- a/src/reducers/eraser-tool.js +++ b/src/reducers/eraser-tool.js @@ -22,4 +22,4 @@ reducer.changeBrushSize = function (brushSize) { }; }; -module.exports = reducer; +export default reducer; diff --git a/src/reducers/tools.js b/src/reducers/tools.js index 59cf1afb..57b7d4c7 100644 --- a/src/reducers/tools.js +++ b/src/reducers/tools.js @@ -1,5 +1,5 @@ -const ToolTypes = require('../tools/tool-types'); -const log = require('../log/log'); +import ToolTypes from '../tools/tool-types'; +import log from '../log/log'; const CHANGE_TOOL = 'scratch-paint/tools/CHANGE_TOOL'; const initialState = ToolTypes.BRUSH; @@ -8,10 +8,10 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_TOOL: - if (action.tool instanceof ToolTypes) { + if (action.tool in ToolTypes) { return action.tool; } - log.warn(`Warning: Tool type does not exist: ${action.tool}`); + log.warn(`Tool type does not exist: ${action.tool}`); /* falls through */ default: return state; @@ -22,11 +22,8 @@ const reducer = function (state, action) { reducer.changeTool = function (tool) { return { type: CHANGE_TOOL, - tool: tool, - meta: { - throttle: 30 - } + tool: tool }; }; -module.exports = reducer; +export default reducer; diff --git a/src/tools/tool-types.js b/src/tools/tool-types.js index 12c8b0f4..9a850d80 100644 --- a/src/tools/tool-types.js +++ b/src/tools/tool-types.js @@ -1,12 +1,8 @@ -class ToolTypes { - constructor (name) { - this.name = name; - } - toString () { - return `ToolTypes.${this.name}`; - } -} -ToolTypes.BRUSH = new ToolTypes('BRUSH'); -ToolTypes.ERASER = new ToolTypes('ERASER'); +import keyMirror from 'keymirror'; -module.exports = ToolTypes; +const ToolTypes = keyMirror({ + BRUSH: null, + ERASER: null +}); + +export default ToolTypes; diff --git a/test/__mocks__/fileMock.js b/test/__mocks__/fileMock.js new file mode 100644 index 00000000..59890f6a --- /dev/null +++ b/test/__mocks__/fileMock.js @@ -0,0 +1,3 @@ +// __mocks__/fileMock.js + +module.exports = 'test-file-stub'; diff --git a/test/__mocks__/styleMock.js b/test/__mocks__/styleMock.js new file mode 100644 index 00000000..d988e23b --- /dev/null +++ b/test/__mocks__/styleMock.js @@ -0,0 +1,3 @@ +// __mocks__/styleMock.js + +module.exports = {}; diff --git a/test/tools-reducer-test.js b/test/tools-reducer-test.js deleted file mode 100644 index 3da061d1..00000000 --- a/test/tools-reducer-test.js +++ /dev/null @@ -1,25 +0,0 @@ -const test = require('tap').test; -const ToolTypes = require('../src/tools/tool-types'); -const reducer = require('../src/reducers/tools'); - -test('initialState', t => { - let defaultState; - t.assert(reducer(defaultState /* state */, {type: 'anything'} /* action */) instanceof ToolTypes); - t.end(); -}); - -test('changeTool', t => { - let defaultState; - t.assert(reducer(defaultState /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */), ToolTypes.ERASER); - t.assert( - reducer(ToolTypes.ERASER /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */), ToolTypes.ERASER); - t.assert(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */), ToolTypes.ERASER); - t.end(); -}); - -test('invalidChangeTool', t => { - t.assert( - reducer(ToolTypes.BRUSH /* state */, reducer.changeTool('non-existant tool') /* action */), ToolTypes.BRUSH); - t.assert(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool() /* action */), ToolTypes.BRUSH); - t.end(); -}); diff --git a/test/unit/tools-reducer.test.js b/test/unit/tools-reducer.test.js new file mode 100644 index 00000000..428b3118 --- /dev/null +++ b/test/unit/tools-reducer.test.js @@ -0,0 +1,23 @@ +/* eslint-env jest */ +import ToolTypes from '../../src/tools/tool-types'; +import reducer from '../../src/reducers/tools'; + +test('initialState', () => { + let defaultState; + expect(reducer(defaultState /* state */, {type: 'anything'} /* action */) in ToolTypes).toBeTruthy(); +}); + +test('changeTool', () => { + let defaultState; + expect(reducer(defaultState /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */)).toBe(ToolTypes.ERASER); + expect(reducer(ToolTypes.ERASER /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */)) + .toBe(ToolTypes.ERASER); + expect(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */)) + .toBe(ToolTypes.ERASER); +}); + +test('invalidChangeTool', () => { + expect(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool('non-existant tool') /* action */)) + .toBe(ToolTypes.BRUSH); + expect(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool() /* action */)).toBe(ToolTypes.BRUSH); +});