#509: Component Object Pattern
This commit is contained in:
45
component-object/pom.xml
Normal file
45
component-object/pom.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>java-design-patterns</artifactId>
|
||||||
|
<version>1.20.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>component-object</artifactId>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.bonigarcia</groupId>
|
||||||
|
<artifactId>webdrivermanager</artifactId>
|
||||||
|
<version>1.4.6</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>1.4.1.RELEASE</version>
|
||||||
|
<scope>import</scope>
|
||||||
|
<type>pom</type>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
</project>
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.iluwatar.component.app;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class TodoApp {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(TodoApp.class, args);
|
||||||
|
}
|
||||||
|
}
|
3380
component-object/src/main/resources/static/dist/bundle.js
vendored
Normal file
3380
component-object/src/main/resources/static/dist/bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
component-object/src/main/resources/static/index.html
Normal file
11
component-object/src/main/resources/static/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Todos</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
</div>
|
||||||
|
<script src="dist/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
component-object/src/main/ui/.babelrc
Normal file
4
component-object/src/main/ui/.babelrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"presets": ["es2015", "react"],
|
||||||
|
"plugins": ["transform-object-rest-spread"]
|
||||||
|
}
|
8
component-object/src/main/ui/.eslintrc
Normal file
8
component-object/src/main/ui/.eslintrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "airbnb",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"experimentalObjectRestSpread": true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
5
component-object/src/main/ui/.gitignore
vendored
Normal file
5
component-object/src/main/ui/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
|
32
component-object/src/main/ui/devServer.js
Normal file
32
component-object/src/main/ui/devServer.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import webpackDevMiddleware from 'webpack-dev-middleware';
|
||||||
|
import config from './webpack.config.babel';
|
||||||
|
import Express from 'express';
|
||||||
|
|
||||||
|
const app = new Express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
app.use(webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, error => {
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable no-console */
|
||||||
|
});
|
11
component-object/src/main/ui/index.html
Normal file
11
component-object/src/main/ui/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Todos</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
</div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
38
component-object/src/main/ui/package.json
Normal file
38
component-object/src/main/ui/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "redux-todos-example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Redux Todos example",
|
||||||
|
"scripts": {
|
||||||
|
"start": "babel-node devServer.js",
|
||||||
|
"lint": "eslint src"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/gaearon/todos.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"babel-polyfill": "^6.3.14",
|
||||||
|
"react": "^15.0.2",
|
||||||
|
"react-dom": "^15.0.2",
|
||||||
|
"react-redux": "^4.4.5",
|
||||||
|
"redux": "^3.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.7.7",
|
||||||
|
"babel-core": "^6.3.15",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.6.5",
|
||||||
|
"babel-preset-es2015": "^6.6.0",
|
||||||
|
"babel-preset-react": "^6.5.0",
|
||||||
|
"babel-register": "^6.3.13",
|
||||||
|
"eslint": "^2.9.0",
|
||||||
|
"eslint-config-airbnb": "^8.0.0",
|
||||||
|
"eslint-plugin-import": "^1.6.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^1.0.4",
|
||||||
|
"eslint-plugin-react": "^5.0.1",
|
||||||
|
"express": "^4.13.3",
|
||||||
|
"webpack": "^1.9.11",
|
||||||
|
"webpack-dev-middleware": "^1.2.0"
|
||||||
|
}
|
||||||
|
}
|
37
component-object/src/main/ui/src/actions/index.js
Normal file
37
component-object/src/main/ui/src/actions/index.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
let nextTodoId = 0;
|
||||||
|
export const addTodo = (text) => {
|
||||||
|
return {
|
||||||
|
type: 'ADD_TODO',
|
||||||
|
id: (nextTodoId++).toString(),
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setVisibilityFilter = (filter) => {
|
||||||
|
return {
|
||||||
|
type: 'SET_VISIBILITY_FILTER',
|
||||||
|
filter,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleTodo = (id) => {
|
||||||
|
return {
|
||||||
|
type: 'TOGGLE_TODO',
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addGroceryItem = (text) => {
|
||||||
|
return {
|
||||||
|
type: 'ADD_GROCERY_ITEM',
|
||||||
|
id: (nextTodoId++).toString(),
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleGroceryItem = (id) => {
|
||||||
|
return {
|
||||||
|
type: 'TOGGLE_GROCERY_ITEM',
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { addGroceryItem } from '../actions';
|
||||||
|
|
||||||
|
const AddGroceryItem = ({ dispatch, className }) => {
|
||||||
|
let input;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<form
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!input.value.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(addGroceryItem(input.value));
|
||||||
|
input.value = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input ref={node => { input = node; }} />
|
||||||
|
<button type="submit">
|
||||||
|
Add Grocery Item
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddGroceryItem.propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
className: PropTypes.func.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect()(AddGroceryItem);
|
34
component-object/src/main/ui/src/components/AddTodo.js
Normal file
34
component-object/src/main/ui/src/components/AddTodo.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { addTodo } from '../actions';
|
||||||
|
|
||||||
|
const AddTodo = ({ dispatch, className }) => {
|
||||||
|
let input;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<form
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!input.value.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(addTodo(input.value));
|
||||||
|
input.value = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input ref={node => { input = node; }} />
|
||||||
|
<button type="submit">
|
||||||
|
Add Todo
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddTodo.propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
className: PropTypes.func.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect()(AddTodo);
|
18
component-object/src/main/ui/src/components/App.js
Normal file
18
component-object/src/main/ui/src/components/App.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Footer from './Footer';
|
||||||
|
import AddTodo from './AddTodo';
|
||||||
|
import VisibleTodoList from './VisibleTodoList';
|
||||||
|
import AddGroceryItem from './AddGroceryItem';
|
||||||
|
import VisibleGroceryList from './VisibleGroceryList';
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<div>
|
||||||
|
<AddTodo className="add-todo"/>
|
||||||
|
<VisibleTodoList className="todo-list"/>
|
||||||
|
<AddGroceryItem className="add-grocery-item"/>
|
||||||
|
<VisibleGroceryList className="grocery-list"/>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
24
component-object/src/main/ui/src/components/FilterLink.js
Normal file
24
component-object/src/main/ui/src/components/FilterLink.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { setVisibilityFilter } from '../actions';
|
||||||
|
import Link from './Link';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
return {
|
||||||
|
active: ownProps.filter === state.visibilityFilter,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||||
|
return {
|
||||||
|
onClick: () => {
|
||||||
|
dispatch(setVisibilityFilter(ownProps.filter));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterLink = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Link);
|
||||||
|
|
||||||
|
export default FilterLink;
|
22
component-object/src/main/ui/src/components/Footer.js
Normal file
22
component-object/src/main/ui/src/components/Footer.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FilterLink from './FilterLink';
|
||||||
|
|
||||||
|
const Footer = () => (
|
||||||
|
<p>
|
||||||
|
Show:
|
||||||
|
{" "}
|
||||||
|
<FilterLink filter="SHOW_ALL">
|
||||||
|
All
|
||||||
|
</FilterLink>
|
||||||
|
{", "}
|
||||||
|
<FilterLink filter="SHOW_ACTIVE">
|
||||||
|
Active
|
||||||
|
</FilterLink>
|
||||||
|
{", "}
|
||||||
|
<FilterLink filter="SHOW_COMPLETED">
|
||||||
|
Completed
|
||||||
|
</FilterLink>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Footer;
|
27
component-object/src/main/ui/src/components/Link.js
Normal file
27
component-object/src/main/ui/src/components/Link.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
const Link = ({ active, children, onClick }) => {
|
||||||
|
if (active) {
|
||||||
|
return <span>{children}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Link.propTypes = {
|
||||||
|
active: PropTypes.bool.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Link;
|
20
component-object/src/main/ui/src/components/Todo.js
Normal file
20
component-object/src/main/ui/src/components/Todo.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
const Todo = ({ onClick, completed, text }) => (
|
||||||
|
<li
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
textDecoration: completed ? 'line-through' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
Todo.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
completed: PropTypes.bool.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Todo;
|
26
component-object/src/main/ui/src/components/TodoList.js
Normal file
26
component-object/src/main/ui/src/components/TodoList.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import Todo from './Todo';
|
||||||
|
|
||||||
|
const TodoList = ({ todos, onTodoClick, className }) => (
|
||||||
|
<ul className={className}>
|
||||||
|
{todos.map(todo =>
|
||||||
|
<Todo
|
||||||
|
key={todo.id}
|
||||||
|
{...todo}
|
||||||
|
onClick={() => onTodoClick(todo.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
TodoList.propTypes = {
|
||||||
|
todos: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
completed: PropTypes.bool.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
}).isRequired).isRequired,
|
||||||
|
onTodoClick: PropTypes.func.isRequired,
|
||||||
|
className: PropTypes.func.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoList;
|
@ -0,0 +1,37 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { toggleGroceryItem } from '../actions';
|
||||||
|
import TodoList from './TodoList';
|
||||||
|
|
||||||
|
const getVisibleGroceryItems = (groceryList, filter) => {
|
||||||
|
switch (filter) {
|
||||||
|
case 'SHOW_ALL':
|
||||||
|
return groceryList;
|
||||||
|
case 'SHOW_COMPLETED':
|
||||||
|
return groceryList.filter(t => t.completed);
|
||||||
|
case 'SHOW_ACTIVE':
|
||||||
|
return groceryList.filter(t => !t.completed);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown filter: ${filter}.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
todos: getVisibleGroceryItems(state.groceryList, state.visibilityFilter),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
onTodoClick: (id) => {
|
||||||
|
dispatch(toggleGroceryItem(id));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisibleGroceryList = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TodoList);
|
||||||
|
|
||||||
|
export default VisibleGroceryList;
|
@ -0,0 +1,37 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { toggleTodo } from '../actions';
|
||||||
|
import TodoList from './TodoList';
|
||||||
|
|
||||||
|
const getVisibleTodos = (todos, filter) => {
|
||||||
|
switch (filter) {
|
||||||
|
case 'SHOW_ALL':
|
||||||
|
return todos;
|
||||||
|
case 'SHOW_COMPLETED':
|
||||||
|
return todos.filter(t => t.completed);
|
||||||
|
case 'SHOW_ACTIVE':
|
||||||
|
return todos.filter(t => !t.completed);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown filter: ${filter}.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
todos: getVisibleTodos(state.todos, state.visibilityFilter),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
onTodoClick: (id) => {
|
||||||
|
dispatch(toggleTodo(id));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisibleTodoList = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TodoList);
|
||||||
|
|
||||||
|
export default VisibleTodoList;
|
16
component-object/src/main/ui/src/index.js
Normal file
16
component-object/src/main/ui/src/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'babel-polyfill';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import todoApp from './reducers';
|
||||||
|
import App from './components/App';
|
||||||
|
|
||||||
|
const store = createStore(todoApp);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
38
component-object/src/main/ui/src/reducers/groceryList.js
Normal file
38
component-object/src/main/ui/src/reducers/groceryList.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const groceryItem = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_GROCERY_ITEM':
|
||||||
|
return {
|
||||||
|
id: action.id,
|
||||||
|
text: action.text,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
case 'TOGGLE_GROCERY_ITEM':
|
||||||
|
if (state.id !== action.id) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completed: !state.completed,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const groceryList = (state = [], action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_GROCERY_ITEM':
|
||||||
|
return [
|
||||||
|
...state,
|
||||||
|
groceryItem(undefined, action),
|
||||||
|
];
|
||||||
|
case 'TOGGLE_GROCERY_ITEM':
|
||||||
|
return state.map(t =>
|
||||||
|
groceryItem(t, action)
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default groceryList;
|
12
component-object/src/main/ui/src/reducers/index.js
Normal file
12
component-object/src/main/ui/src/reducers/index.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import todos from './todos';
|
||||||
|
import groceryList from './groceryList';
|
||||||
|
import visibilityFilter from './visibilityFilter';
|
||||||
|
|
||||||
|
const todoApp = combineReducers({
|
||||||
|
todos,
|
||||||
|
groceryList,
|
||||||
|
visibilityFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default todoApp;
|
38
component-object/src/main/ui/src/reducers/todos.js
Normal file
38
component-object/src/main/ui/src/reducers/todos.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const todo = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_TODO':
|
||||||
|
return {
|
||||||
|
id: action.id,
|
||||||
|
text: action.text,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
case 'TOGGLE_TODO':
|
||||||
|
if (state.id !== action.id) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completed: !state.completed,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const todos = (state = [], action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_TODO':
|
||||||
|
return [
|
||||||
|
...state,
|
||||||
|
todo(undefined, action),
|
||||||
|
];
|
||||||
|
case 'TOGGLE_TODO':
|
||||||
|
return state.map(t =>
|
||||||
|
todo(t, action)
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default todos;
|
@ -0,0 +1,10 @@
|
|||||||
|
const visibilityFilter = (state = 'SHOW_ALL', action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_VISIBILITY_FILTER':
|
||||||
|
return action.filter;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default visibilityFilter;
|
19
component-object/src/main/ui/webpack.config.babel.js
Normal file
19
component-object/src/main/ui/webpack.config.babel.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
devtool: 'eval',
|
||||||
|
entry: './src/index',
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, '../resources/static/dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/static/',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
include: __dirname,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.iluwatar.component;
|
||||||
|
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
|
||||||
|
class AddItemComponent {
|
||||||
|
private WebDriver driver;
|
||||||
|
private String containerCssSelector;
|
||||||
|
|
||||||
|
AddItemComponent(WebDriver driver, String containerCssSelector) {
|
||||||
|
this.driver = driver;
|
||||||
|
this.containerCssSelector = containerCssSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddItemComponent addItem(String todo) {
|
||||||
|
WebElement input = driver.findElement(By.cssSelector(containerCssSelector + " input"));
|
||||||
|
input.sendKeys(todo);
|
||||||
|
WebElement button = driver.findElement(By.cssSelector(containerCssSelector + " button"));
|
||||||
|
button.click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.iluwatar.component;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
class ItemsListComponent {
|
||||||
|
private final WebDriver driver;
|
||||||
|
private final String containerCssSelector;
|
||||||
|
|
||||||
|
ItemsListComponent(WebDriver driver, String containerCssSelector) {
|
||||||
|
this.driver = driver;
|
||||||
|
this.containerCssSelector = containerCssSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsListComponent clickOnItem(String todoItem) {
|
||||||
|
findElementWithText(todoItem).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsListComponent verifyItemShown(String todoItem, boolean expectedStrikethrough) {
|
||||||
|
WebElement todoElement = findElementWithText(todoItem);
|
||||||
|
assertNotNull(todoElement);
|
||||||
|
boolean actualStrikethrough = todoElement.getAttribute("style").contains("text-decoration: line-through;");
|
||||||
|
assertEquals(expectedStrikethrough, actualStrikethrough);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsListComponent verifyItemNotShown(String todoItem) {
|
||||||
|
assertTrue(findElementsWithText(todoItem).isEmpty());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebElement findElementWithText(String text) {
|
||||||
|
return driver.findElement(getConditionForText(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WebElement> findElementsWithText(String text) {
|
||||||
|
return driver.findElements(getConditionForText(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private By getConditionForText(String text) {
|
||||||
|
String containerClassName = StringUtils.substring(containerCssSelector, 1);
|
||||||
|
return By.xpath(format("//*[@class='" + containerClassName + "']//*[text()='%s']", text));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
package com.iluwatar.component;
|
||||||
|
|
||||||
|
import io.github.bonigarcia.wdm.ChromeDriverManager;
|
||||||
|
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
|
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||||
|
import org.springframework.boot.test.WebIntegrationTest;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
import com.iluwatar.component.app.TodoApp;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@SpringApplicationConfiguration(classes = TodoApp.class)
|
||||||
|
@WebIntegrationTest
|
||||||
|
public class TodoAppTest {
|
||||||
|
private static WebDriver driver;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() {
|
||||||
|
ChromeDriverManager.getInstance().setup();
|
||||||
|
driver = new ChromeDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void tearDown() {
|
||||||
|
driver.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateTodos() {
|
||||||
|
// GIVEN
|
||||||
|
new TodoPageObject(driver).get()
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
.addTodo("Buy groceries")
|
||||||
|
.addTodo("Tidy up")
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getTodoList()
|
||||||
|
.verifyItemShown("Buy groceries", false)
|
||||||
|
.verifyItemShown("Tidy up", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompleteTodo() {
|
||||||
|
// GIVEN
|
||||||
|
new TodoPageObject(driver).get()
|
||||||
|
.addTodo("Buy groceries")
|
||||||
|
.addTodo("Tidy up")
|
||||||
|
.getTodoList()
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
.clickOnItem("Buy groceries")
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.verifyItemShown("Buy groceries", true)
|
||||||
|
.verifyItemShown("Tidy up", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectTodosActive() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
|
||||||
|
todoPage
|
||||||
|
.addTodo("Buy groceries")
|
||||||
|
.addTodo("Tidy up")
|
||||||
|
.getTodoList()
|
||||||
|
.clickOnItem("Buy groceries");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
todoPage
|
||||||
|
.selectActive()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getTodoList()
|
||||||
|
.verifyItemNotShown("Buy groceries")
|
||||||
|
.verifyItemShown("Tidy up", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectTodosCompleted() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
todoPage
|
||||||
|
.addTodo("Buy groceries")
|
||||||
|
.addTodo("Tidy up")
|
||||||
|
.getTodoList()
|
||||||
|
.clickOnItem("Buy groceries");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
todoPage
|
||||||
|
.selectCompleted()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getTodoList()
|
||||||
|
.verifyItemShown("Buy groceries", true)
|
||||||
|
.verifyItemNotShown("Tidy up");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectTodosAll() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
todoPage
|
||||||
|
.addTodo("Buy groceries")
|
||||||
|
.addTodo("Tidy up")
|
||||||
|
.getTodoList()
|
||||||
|
.clickOnItem("Buy groceries");
|
||||||
|
todoPage
|
||||||
|
.selectCompleted()
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
.selectAll()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getTodoList()
|
||||||
|
.verifyItemShown("Buy groceries", true)
|
||||||
|
.verifyItemShown("Tidy up", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateGroceryItems() {
|
||||||
|
// GIVEN
|
||||||
|
new TodoPageObject(driver).get()
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
.addGroceryItem("avocados")
|
||||||
|
.addGroceryItem("tomatoes")
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getGroceryList()
|
||||||
|
.verifyItemShown("avocados", false)
|
||||||
|
.verifyItemShown("tomatoes", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompleteGroceryItem() {
|
||||||
|
// GIVEN
|
||||||
|
new TodoPageObject(driver).get()
|
||||||
|
.addGroceryItem("avocados")
|
||||||
|
.addGroceryItem("tomatoes")
|
||||||
|
.getGroceryList()
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
.clickOnItem("avocados")
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.verifyItemShown("avocados", true)
|
||||||
|
.verifyItemShown("tomatoes", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectGroceryItemsActive() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
|
||||||
|
todoPage
|
||||||
|
.addGroceryItem("avocados")
|
||||||
|
.addGroceryItem("tomatoes")
|
||||||
|
.getGroceryList()
|
||||||
|
.clickOnItem("avocados");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
todoPage
|
||||||
|
.selectActive()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getGroceryList()
|
||||||
|
.verifyItemNotShown("avocados")
|
||||||
|
.verifyItemShown("tomatoes", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectGroceryItemsCompleted() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
todoPage
|
||||||
|
.addGroceryItem("avocados")
|
||||||
|
.addGroceryItem("tomatoes")
|
||||||
|
.getGroceryList()
|
||||||
|
.clickOnItem("avocados");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
todoPage
|
||||||
|
.selectCompleted()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getGroceryList()
|
||||||
|
.verifyItemShown("avocados", true)
|
||||||
|
.verifyItemNotShown("tomatoes");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectGroceryItemsAll() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
todoPage
|
||||||
|
.addGroceryItem("avocados")
|
||||||
|
.addGroceryItem("tomatoes")
|
||||||
|
.getGroceryList()
|
||||||
|
.clickOnItem("avocados");
|
||||||
|
todoPage
|
||||||
|
.selectCompleted()
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
.selectAll()
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
.getGroceryList()
|
||||||
|
.verifyItemShown("avocados", true)
|
||||||
|
.verifyItemShown("tomatoes", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSelectCombinedItemsActive() {
|
||||||
|
// GIVEN
|
||||||
|
TodoPageObject todoPage = new TodoPageObject(driver).get();
|
||||||
|
|
||||||
|
todoPage
|
||||||
|
.addTodo("Buy groceries")
|
||||||
|
.addTodo("Tidy up")
|
||||||
|
.addGroceryItem("avocados")
|
||||||
|
.addGroceryItem("tomatoes");
|
||||||
|
|
||||||
|
todoPage
|
||||||
|
.getGroceryList()
|
||||||
|
.clickOnItem("avocados");
|
||||||
|
|
||||||
|
todoPage
|
||||||
|
.getTodoList()
|
||||||
|
.clickOnItem("Tidy up");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
todoPage
|
||||||
|
.selectActive();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
todoPage
|
||||||
|
.getTodoList()
|
||||||
|
.verifyItemShown("Buy groceries", false)
|
||||||
|
.verifyItemNotShown("Tidy up");
|
||||||
|
|
||||||
|
todoPage
|
||||||
|
.getGroceryList()
|
||||||
|
.verifyItemNotShown("avocados")
|
||||||
|
.verifyItemShown("tomatoes", false);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package com.iluwatar.component;
|
||||||
|
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||||
|
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
class TodoPageObject {
|
||||||
|
private final WebDriver driver;
|
||||||
|
private final WebDriverWait wait;
|
||||||
|
private final ItemsListComponent todoItemsList;
|
||||||
|
private final AddItemComponent addTodoItemComponent;
|
||||||
|
private final ItemsListComponent groceryItemsList;
|
||||||
|
private final AddItemComponent addGroceryItemComponent;
|
||||||
|
|
||||||
|
TodoPageObject(WebDriver driver) {
|
||||||
|
this.driver = driver;
|
||||||
|
this.wait = new WebDriverWait(driver, 10);
|
||||||
|
todoItemsList = new ItemsListComponent(driver, ".todo-list");
|
||||||
|
addTodoItemComponent = new AddItemComponent(driver, ".add-todo");
|
||||||
|
groceryItemsList = new ItemsListComponent(driver, ".grocery-list");
|
||||||
|
addGroceryItemComponent = new AddItemComponent(driver, ".add-grocery-item");
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoPageObject get() {
|
||||||
|
driver.get("localhost:8080");
|
||||||
|
wait.until(ExpectedConditions.elementToBeClickable(By.tagName("button")));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoPageObject selectAll() {
|
||||||
|
findElementWithText("All").click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoPageObject selectActive() {
|
||||||
|
findElementWithText("Active").click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoPageObject selectCompleted() {
|
||||||
|
findElementWithText("Completed").click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoPageObject addTodo(String todoName) {
|
||||||
|
addTodoItemComponent.addItem(todoName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoPageObject addGroceryItem(String todoName) {
|
||||||
|
addGroceryItemComponent.addItem(todoName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsListComponent getTodoList() {
|
||||||
|
return todoItemsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemsListComponent getGroceryList() {
|
||||||
|
return groceryItemsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebElement findElementWithText(String text) {
|
||||||
|
return driver.findElement(getConditionForText(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private By getConditionForText(String text) {
|
||||||
|
return By.xpath(format("//*[text()='%s']", text));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user