#509: Component Object Pattern

This commit is contained in:
nikhilbarar
2018-06-11 00:38:03 +05:30
parent 987994f0fe
commit 971a74e13a
33 changed files with 4407 additions and 1 deletions

45
component-object/pom.xml Normal file
View 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>

View File

@ -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);
}
}

File diff suppressed because one or more lines are too long

View 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>

View File

@ -0,0 +1,4 @@
{
"presets": ["es2015", "react"],
"plugins": ["transform-object-rest-spread"]
}

View File

@ -0,0 +1,8 @@
{
"extends": "airbnb",
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true
},
},
}

View File

@ -0,0 +1,5 @@
.DS_Store
*.log
dist
node_modules

View 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 */
});

View 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>

View 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"
}
}

View 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,
};
};

View File

@ -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);

View 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);

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View 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')
);

View 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;

View 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;

View 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;

View File

@ -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;

View 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,
}],
},
};

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -161,6 +161,7 @@
<module>dirty-flag</module>
<module>trampoline</module>
<module>serverless</module>
<module>component-object</module>
</modules>
<repositories>