Initial commit
This commit is contained in:
20
frontend/src/actions/chat.js
Normal file
20
frontend/src/actions/chat.js
Normal file
@ -0,0 +1,20 @@
|
||||
export const SEND_MESSAGE = 'SEND_MESSAGE'
|
||||
export const SEND_MESSAGE_SUCCESS = 'SEND_MESSAGE_SUCCESS';
|
||||
export const SEND_MESSAGE_FAILURE = 'SEND_MESSAGE_FAILURE';
|
||||
|
||||
export function sendMessage (channel, text) {
|
||||
return { type: SEND_MESSAGE, channel, text }
|
||||
}
|
||||
|
||||
export const FETCH_MESSAGES = 'FETCH_MESSAGES'
|
||||
export const FETCH_MESSAGES_SUCCESS = 'FETCH_MESSAGES_SUCCESS';
|
||||
export const FETCH_MESSAGES_FAILURE = 'FETCH_MESSAGES_FAILURE';
|
||||
|
||||
export function fetchMessages (channel) {
|
||||
return { type: FETCH_MESSAGES, channel }
|
||||
}
|
||||
|
||||
export const STREAM_EVENTS = 'STREAM_EVENTS'
|
||||
export function streamEvents (channel) {
|
||||
return { type: STREAM_EVENTS, channel }
|
||||
}
|
7
frontend/src/actions/login.js
Normal file
7
frontend/src/actions/login.js
Normal file
@ -0,0 +1,7 @@
|
||||
export const LOGIN = 'LOGIN'
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
|
||||
|
||||
export function login (username, password) {
|
||||
return { type: LOGIN, username, password }
|
||||
}
|
11
frontend/src/actions/products.js
Normal file
11
frontend/src/actions/products.js
Normal file
@ -0,0 +1,11 @@
|
||||
export const ADD_PRODUCT = 'ADD_PRODUCT'
|
||||
|
||||
export function addProduct (name, price) {
|
||||
return {type: ADD_PRODUCT, product: {name, price}}
|
||||
}
|
||||
|
||||
export const REMOVE_PRODUCT = 'REMOVE_PRODUCT'
|
||||
|
||||
export function removeProduct (name) {
|
||||
return {type: REMOVE_PRODUCT, productName: name}
|
||||
}
|
28
frontend/src/app.js
Normal file
28
frontend/src/app.js
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
import { Component, Fragment } from 'react'
|
||||
import { hot } from 'react-hot-loader'
|
||||
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
|
||||
import { Route, Switch, Redirect } from 'react-router'
|
||||
import LoginPage from './pages/login';
|
||||
import ChatPage from './pages/chat';
|
||||
|
||||
require('bulma/css/bulma.min.css')
|
||||
|
||||
class App extends Component {
|
||||
render () {
|
||||
return (
|
||||
<Fragment>
|
||||
<HashRouter>
|
||||
<Switch>
|
||||
<Route path='/login' exact component={LoginPage} />
|
||||
<Route path='/chat/:channel' component={ChatPage} />
|
||||
<Route path='/chat' component={ChatPage} />
|
||||
<Route component={() => <Redirect to="/login" />} />
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(App)
|
21
frontend/src/components/__snapshots__/counter.test.js.snap
Normal file
21
frontend/src/components/__snapshots__/counter.test.js.snap
Normal file
@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Counter snapshot 1`] = `
|
||||
<div>
|
||||
Count:
|
||||
<span>
|
||||
0
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
-1
|
||||
</button>
|
||||
</div>
|
||||
`;
|
32
frontend/src/components/clock.js
Normal file
32
frontend/src/components/clock.js
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Clock extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
// Initialisation du "state" du composant
|
||||
this.state = {
|
||||
time: new Date(),
|
||||
foo: "bar"
|
||||
}
|
||||
|
||||
this.tick = this.tick.bind(this);
|
||||
// On appelle la méthode tick() du composant
|
||||
// toutes les secondes
|
||||
setInterval(this.tick, this.props.interval);
|
||||
}
|
||||
|
||||
// Méthode de rendu du composant
|
||||
render() {
|
||||
return (
|
||||
<div>Time: { this.state.time.toString() }</div>
|
||||
)
|
||||
}
|
||||
|
||||
// La méthode tick() met à jour le state du composant avec
|
||||
// la date courante
|
||||
tick() {
|
||||
this.setState({ time: new Date() });
|
||||
}
|
||||
|
||||
}
|
33
frontend/src/components/counter.js
Normal file
33
frontend/src/components/counter.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
// Initialisation du "state" du composant
|
||||
this.state = {
|
||||
count: 0
|
||||
}
|
||||
// On "lie" les méthodes de la classe à l'instance
|
||||
this.increment = this.increment.bind(this)
|
||||
this.decrement = this.decrement.bind(this)
|
||||
}
|
||||
// Méthode de rendu du composant
|
||||
render() {
|
||||
console.log(this.props.match);
|
||||
return (
|
||||
<div>
|
||||
Count: <span>{ this.state.count }</span>
|
||||
<button onClick={ this.increment }>+1</button>
|
||||
<button onClick={ this.decrement }>-1</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// La méthode increment() incrémente la valeur du compteur de 1
|
||||
increment() {
|
||||
this.setState(prevState => ({ count: prevState.count+1 }))
|
||||
}
|
||||
// La méthode decrement() décrémente la valeur du compteur de 1
|
||||
decrement() {
|
||||
this.setState(prevState => ({ count: prevState.count-1 }))
|
||||
}
|
||||
}
|
26
frontend/src/components/counter.test.js
Normal file
26
frontend/src/components/counter.test.js
Normal file
@ -0,0 +1,26 @@
|
||||
/* globals test, expect */
|
||||
import React from 'react';
|
||||
import Counter from './counter'
|
||||
import renderer from 'react-test-renderer'
|
||||
|
||||
test('Counter snapshot', () => {
|
||||
|
||||
const component = renderer.create(<Counter />)
|
||||
|
||||
let tree = component.toJSON()
|
||||
|
||||
// Vérifier que le composant n'a pas changé depuis le dernier
|
||||
// snapshot.
|
||||
// Voir https://facebook.github.io/jest/docs/en/snapshot-testing.html
|
||||
// pour plus d'informations
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
// L'API expect() de Jest est disponible à l'adresse
|
||||
// https://facebook.github.io/jest/docs/en/expect.html
|
||||
|
||||
// Il est possible d'effectuer des vérifications plus avancées
|
||||
// grâce au projet Enzyme (vérification du DOM, etc)
|
||||
// Voir http://airbnb.io/enzyme/ et
|
||||
// https://facebook.github.io/jest/docs/en/tutorial-react.html#dom-testing
|
||||
|
||||
})
|
41
frontend/src/components/myform.js
Normal file
41
frontend/src/components/myform.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { addProduct } from '../actions/products'
|
||||
|
||||
|
||||
class MyForm extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {name: ''};
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleChange(evt) {
|
||||
this.setState({ name: evt.target.value });
|
||||
}
|
||||
|
||||
handleSubmit(evt) {
|
||||
console.log(`Votre nom est ${this.state.name}`);
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(addProduct('pomme', 10));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
Nom:
|
||||
<input type="text" value={this.state.name} onChange={this.handleChange} />
|
||||
</label>
|
||||
<input type="submit" value="Soumettre" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(MyForm)
|
12
frontend/src/index.html
Normal file
12
frontend/src/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Insight</title>
|
||||
<% for (var css in htmlWebpackPlugin.files.css) { %>
|
||||
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
|
||||
<% } %>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
13
frontend/src/index.js
Normal file
13
frontend/src/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './app'
|
||||
import { configureStore } from './store/store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
const store = configureStore()
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
)
|
82
frontend/src/pages/chat.jsx
Normal file
82
frontend/src/pages/chat.jsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import Page from './page';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchMessages, sendMessage, streamEvents } from '../actions/chat';
|
||||
|
||||
export class ChatPage extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
message: ''
|
||||
}
|
||||
this.handleSendMessage = this.handleSendMessage.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const channel = "default";
|
||||
const { chat } = this.props;
|
||||
const messages = channel in chat.messagesByChannel ? chat.messagesByChannel[channel] : [];
|
||||
const users = [];
|
||||
return (
|
||||
<Page>
|
||||
<div className="columns">
|
||||
<div className="column is-8 is-offset-2">
|
||||
<div className="box">
|
||||
<div className="tile">
|
||||
<div className="tile is-parent is-vertical is-10">
|
||||
<div className="tile is-child">
|
||||
<b>Chat</b>
|
||||
<ul>
|
||||
{
|
||||
messages.map(msg => {
|
||||
return (
|
||||
<li key={msg.ID}><span className="is-uppercase">[{msg.Username}]</span> <span>{msg.Text}</span></li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input className="input" type="text" placeholder="Hello everyone !"
|
||||
value={this.state.message}
|
||||
onChange={this.handleSendMessage}
|
||||
onKeyDown={this.handleKeyDown} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tile is-parent is-2">
|
||||
<b>Utilisateurs</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchMessages("default"));
|
||||
this.props.dispatch(streamEvents("default"));
|
||||
}
|
||||
|
||||
handleSendMessage(evt) {
|
||||
this.setState({ message: evt.target.value });
|
||||
}
|
||||
|
||||
handleKeyDown(evt) {
|
||||
if (evt.keyCode !== 13) return;
|
||||
this.props.dispatch(sendMessage("default", this.state.message));
|
||||
this.setState({message: ""});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(state => {
|
||||
return {
|
||||
chat: state.chat
|
||||
}
|
||||
})(ChatPage)
|
53
frontend/src/pages/login.jsx
Normal file
53
frontend/src/pages/login.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import Page from './page';
|
||||
import { connect } from 'react-redux';
|
||||
import { login } from '../actions/login';
|
||||
|
||||
export class LoginPage extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.login = this.login.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
<div className="columns">
|
||||
<div className="column is-4 is-offset-4">
|
||||
<div className="box">
|
||||
<div className="field">
|
||||
<label className="label">Login</label>
|
||||
<div className="control">
|
||||
<input type="text" className="input" placeholder="My login..." />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Password</label>
|
||||
<div className="control">
|
||||
<input className="input" type="password" />
|
||||
</div>
|
||||
</div>
|
||||
<button className="button is-primary" onClick={this.login}>Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.user.isLoggedIn) this.props.history.push("/chat");
|
||||
}
|
||||
|
||||
login() {
|
||||
this.props.dispatch(login("foo", "bar"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(state => {
|
||||
return {
|
||||
user: state.user
|
||||
}
|
||||
})(LoginPage)
|
11
frontend/src/pages/page.js
Normal file
11
frontend/src/pages/page.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Page extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
35
frontend/src/reducers/chat.js
Normal file
35
frontend/src/reducers/chat.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/login';
|
||||
import { FETCH_MESSAGES_SUCCESS } from '../actions/chat';
|
||||
|
||||
const defaultState = {
|
||||
messagesByChannel: {},
|
||||
}
|
||||
|
||||
export default function chatReducer(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
case FETCH_MESSAGES_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
messagesByChannel: {
|
||||
...state.messagesByChannel,
|
||||
[action.channel]: [...action.data.Messages]
|
||||
}
|
||||
};
|
||||
case 'CHANNEL_EVENT':
|
||||
switch(action.event) {
|
||||
case "message":
|
||||
return {
|
||||
...state,
|
||||
messagesByChannel: {
|
||||
...state.messagesByChannel,
|
||||
[action.data.Channel]: [
|
||||
...state.messagesByChannel[action.data.Channel],
|
||||
action.data.Message
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
16
frontend/src/reducers/login.js
Normal file
16
frontend/src/reducers/login.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/login';
|
||||
|
||||
const defaultState = {
|
||||
isLoggedIn: false,
|
||||
username: null,
|
||||
}
|
||||
|
||||
export default function loginReducer(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
case LOGIN_SUCCESS:
|
||||
return { ...state, isLoggedIn: true, username: action.data.Username };
|
||||
case LOGIN_FAILURE:
|
||||
return { ...state, isLoggedIn: false, username: null };
|
||||
}
|
||||
return state;
|
||||
}
|
31
frontend/src/reducers/root.js
Normal file
31
frontend/src/reducers/root.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { ADD_PRODUCT, REMOVE_PRODUCT } from '../actions/products';
|
||||
|
||||
export const rootReducer = (state, action) => {
|
||||
|
||||
console.log(`Action: ${JSON.stringify(action)}`)
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case ADD_PRODUCT:
|
||||
// L'action est de type ADD_PRODUCT
|
||||
// On ajoute le produit dans la liste et
|
||||
// on retourne un nouvel état modifié
|
||||
return {
|
||||
products: [...state.products, action.product]
|
||||
}
|
||||
|
||||
|
||||
case REMOVE_PRODUCT:
|
||||
// L'action est de type REMOVE_PRODUCT
|
||||
// On filtre la liste des produits et on
|
||||
// retourne un nouvel état modifié
|
||||
return {
|
||||
products: state.products.filter(p => p.name !== action.productName)
|
||||
}
|
||||
}
|
||||
|
||||
// Si l'action n'est pas gérée, on retourne l'état
|
||||
// sans le modifier
|
||||
return state
|
||||
|
||||
}
|
98
frontend/src/sagas/chat.js
Normal file
98
frontend/src/sagas/chat.js
Normal file
@ -0,0 +1,98 @@
|
||||
import { call, put, take } from 'redux-saga/effects';
|
||||
import { eventChannel } from 'redux-saga'
|
||||
import {
|
||||
SEND_MESSAGE_FAILURE, SEND_MESSAGE_SUCCESS,
|
||||
FETCH_MESSAGES_FAILURE, FETCH_MESSAGES_SUCCESS
|
||||
} from '../actions/chat';
|
||||
|
||||
export function* sendMessageSaga(action) {
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = yield call(sendMessage, action.channel, action.text);
|
||||
} catch(err) {
|
||||
yield put({ type: SEND_MESSAGE_FAILURE, err });
|
||||
}
|
||||
|
||||
if ('Error' in result) {
|
||||
yield put({type: SEND_MESSAGE_FAILURE, err: result.Error});
|
||||
return
|
||||
}
|
||||
|
||||
yield put({type: SEND_MESSAGE_SUCCESS, data: result.Data });
|
||||
|
||||
}
|
||||
|
||||
|
||||
function sendMessage(channel, text) {
|
||||
return fetch(`http://192.168.0.126:3000/channels/${channel}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
Text: text,
|
||||
}),
|
||||
mode: 'cors',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
export function* fetchMessagesSaga(action) {
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = yield call(fetchMessages, action.channel);
|
||||
} catch(err) {
|
||||
yield put({ type: FETCH_MESSAGES_FAILURE, err });
|
||||
}
|
||||
|
||||
if ('Error' in result) {
|
||||
yield put({type: FETCH_MESSAGES_FAILURE, err: result.Error});
|
||||
return
|
||||
}
|
||||
|
||||
yield put({type: FETCH_MESSAGES_SUCCESS, channel: action.channel, data: result.Data });
|
||||
|
||||
}
|
||||
|
||||
|
||||
function fetchMessages(channel) {
|
||||
return fetch(`http://192.168.0.126:3000/channels/${channel}`, {
|
||||
mode: 'cors',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
function channelEvents(channel) {
|
||||
return eventChannel(emitter => {
|
||||
|
||||
const eventSource = new EventSource(
|
||||
`http://192.168.0.126:3000/channels/${channel}/stream`,
|
||||
{ withCredentials: true }
|
||||
);
|
||||
|
||||
const emit = evt => {
|
||||
emitter({type: evt.type, data: evt.data});
|
||||
};
|
||||
|
||||
eventSource.addEventListener("joined", emit);
|
||||
eventSource.addEventListener("left", emit);
|
||||
eventSource.addEventListener("message", emit);
|
||||
|
||||
return () => {
|
||||
eventSource.removeEventListener("joined", emit)
|
||||
eventSource.removeEventListener("left", emit)
|
||||
eventSource.removeEventListener("message", emit)
|
||||
eventSource.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function* streamEventsSaga(action) {
|
||||
const stream = yield call(channelEvents, action.channel)
|
||||
while (true) {
|
||||
let event = yield take(stream)
|
||||
yield put({ type: 'CHANNEL_EVENT', event: event.type, data: JSON.parse(event.data) });
|
||||
}
|
||||
}
|
34
frontend/src/sagas/login.js
Normal file
34
frontend/src/sagas/login.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { LOGIN_FAILURE, LOGIN_SUCCESS } from '../actions/login';
|
||||
|
||||
export default function* loginSaga(action) {
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = yield call(doLogin, action.username, action.password);
|
||||
} catch(err) {
|
||||
yield put({ type: LOGIN_FAILURE, err });
|
||||
}
|
||||
|
||||
if ('Error' in result) {
|
||||
yield put({type: LOGIN_FAILURE, err: result.Error});
|
||||
return
|
||||
}
|
||||
|
||||
yield put({type: LOGIN_SUCCESS, data: result.Data });
|
||||
|
||||
}
|
||||
|
||||
|
||||
function doLogin(username, password) {
|
||||
return fetch('http://192.168.0.126:3000/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
Username: username,
|
||||
Password: password
|
||||
}),
|
||||
mode: 'cors',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(res => res.json())
|
||||
}
|
14
frontend/src/sagas/root.js
Normal file
14
frontend/src/sagas/root.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { all, takeLatest } from 'redux-saga/effects';
|
||||
import loginSaga from './login';
|
||||
import { LOGIN } from '../actions/login';
|
||||
import { SEND_MESSAGE, FETCH_MESSAGES, STREAM_EVENTS } from '../actions/chat';
|
||||
import { sendMessageSaga, fetchMessagesSaga, streamEventsSaga } from './chat';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
takeLatest(LOGIN, loginSaga),
|
||||
takeLatest(SEND_MESSAGE, sendMessageSaga),
|
||||
takeLatest(FETCH_MESSAGES, fetchMessagesSaga),
|
||||
takeLatest(STREAM_EVENTS, streamEventsSaga)
|
||||
]);
|
||||
}
|
26
frontend/src/store/store.js
Normal file
26
frontend/src/store/store.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
|
||||
import loginReducer from '../reducers/login'
|
||||
import rootSaga from '../sagas/root'
|
||||
import createSagaMiddleware from 'redux-saga'
|
||||
import chatReducer from '../reducers/chat';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware()
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
user: loginReducer,
|
||||
chat: chatReducer,
|
||||
});
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
export function configureStore(initialState = {}) {
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
composeEnhancers(
|
||||
applyMiddleware(sagaMiddleware)
|
||||
)
|
||||
)
|
||||
sagaMiddleware.run(rootSaga);
|
||||
return store;
|
||||
}
|
39
frontend/src/store/store.test.js
Normal file
39
frontend/src/store/store.test.js
Normal file
@ -0,0 +1,39 @@
|
||||
/* globals test, expect, jest */
|
||||
import { addProduct, removeProduct } from '../actions/products'
|
||||
import { configureStore } from './store'
|
||||
|
||||
test('Ajout/suppression des produits', () => {
|
||||
// On crée une instance de notre store
|
||||
// avec le state par défaut
|
||||
const store = configureStore()
|
||||
|
||||
// On crée un "faux" subscriber
|
||||
// pour vérifier que l'état du store
|
||||
// a bien été modifié le nombre de fois voulu
|
||||
const subscriber = jest.fn()
|
||||
|
||||
// On attache notre faux subscriber
|
||||
// au store
|
||||
store.subscribe(subscriber)
|
||||
|
||||
// On "dispatch" nos actions
|
||||
store.dispatch(addProduct('pomme', 5))
|
||||
store.dispatch(addProduct('orange', 7))
|
||||
store.dispatch(addProduct('orange', 10))
|
||||
store.dispatch(removeProduct('pomme'))
|
||||
|
||||
// On s'assure que notre subscriber a bien été
|
||||
// appelé
|
||||
expect(subscriber).toHaveBeenCalledTimes(4)
|
||||
|
||||
const state = store.getState()
|
||||
|
||||
// On s'assure que l'état du store correspond
|
||||
// à ce qu'on attend
|
||||
expect(state).toMatchObject({
|
||||
products: [
|
||||
{name: 'orange', price: 7}
|
||||
]
|
||||
})
|
||||
|
||||
})
|
Reference in New Issue
Block a user