#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