Skip to content

Commit

Permalink
Redux with stub data (fixes #8)
Browse files Browse the repository at this point in the history
  • Loading branch information
mstriemer committed Mar 7, 2016
1 parent 27650c4 commit 6f033db
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 33 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"express": "4.13.4",
"helmet": "1.3.0",
"react": "0.14.7",
"react-router": "2.0.0"
"react-redux": "4.4.0",
"react-router": "2.0.0",
"redux": "3.3.1"
},
"devDependencies": {
"babel-core": "6.6.5",
Expand All @@ -44,6 +46,7 @@
"babel-plugin-transform-class-properties": "6.6.0",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-object-rest-spread": "6.6.5",
"babel-polyfill": "6.6.1",
"babel-preset-es2015": "6.6.0",
"babel-preset-react": "6.5.0",
"babel-preset-stage-2": "6.5.0",
Expand All @@ -68,6 +71,8 @@
"react-dom": "0.14.7",
"react-hot-loader": "1.3.0",
"react-transform-hmr": "1.0.4",
"react-transform-hmr": "1.0.2",
"redux-devtools": "3.1.1",
"semver": "5.1.0",
"shelljs": "0.6.0",
"sinon": "1.17.3",
Expand Down
15 changes: 15 additions & 0 deletions src/core/reducers/addons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const initialState = {
wat: {slug: 'wat', title: 'Wat is this?'},
foo: {slug: 'foo', title: 'The foo add-on'},
food: {slug: 'food', title: 'Find food'},
bar: {slug: 'bar', title: 'The bar add-on'},
};

export default function addon(state = initialState, action) {
switch (action.type) {
case 'ADDON_FETCHED':
return Object.assign({}, state, {[action.addon.slug]: action.addon});
default:
return state;
}
}
6 changes: 6 additions & 0 deletions src/search/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function setQuery(query) {
return {
type: 'SET_QUERY',
query,
};
}
1 change: 1 addition & 0 deletions src/search/client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import { Router, browserHistory } from 'react-router';
Expand Down
42 changes: 14 additions & 28 deletions src/search/components/App.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
import React from 'react';

import SearchForm from './SearchForm';
import SearchResults from './SearchResults';
import { gettext as _ } from 'core/utils';

export default class App extends React.Component {

state = {
query: null,
results: [],
}

handleSearch = (query) => {
const results = [{title: 'Foo'}, {title: 'Bar'}, {title: 'Baz'}];
this.setState({ query, results });
}

render() {
const { query, results } = this.state;
return (
<div className="search-app">
<h1>{_('Add-on Search')}</h1>
<SearchForm onSearch={this.handleSearch} />
<SearchResults results={results} query={query} />
</div>
);
}
}
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import CurrentSearchPage from '../containers/CurrentSearchPage';
import search from '../reducers/search';
import addons from 'core/reducers/addons';
const store = createStore(combineReducers({addons, search}));

const App = () => (
<Provider store={store}>
<CurrentSearchPage />
</Provider>
);

export default App;
24 changes: 24 additions & 0 deletions src/search/components/SearchPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { PropTypes } from 'react';

import SearchForm from './SearchForm';
import SearchResults from './SearchResults';
import { gettext as _ } from 'core/utils';

const SearchPage = ({ handleSearch, results, query }) => (
<div className="search-page">
<h1>{_('Add-on Search')}</h1>
<SearchForm onSearch={handleSearch} />
<SearchResults results={results} query={query} />
</div>
);

SearchPage.propTypes = {
handleSearch: PropTypes.func.isRequired,
results: PropTypes.arrayOf(PropTypes.shape({
slug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
})),
query: PropTypes.string,
};

export default SearchPage;
24 changes: 24 additions & 0 deletions src/search/containers/CurrentSearchPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import SearchPage from '../components/SearchPage';
import { setQuery } from '../actions';
import { getMatchingAddons } from 'search/reducers/search';

export function mapStateToProps(state) {
return {
results: getMatchingAddons(state.addons, state.search.query),
query: state.search.query,
};
}

export function mapDispatchToProps(dispatch) {
return {
handleSearch: (query) => dispatch(setQuery(query)),
};
}

const CurrentSearchPage = connect(
mapStateToProps,
mapDispatchToProps,
)(SearchPage);

export default CurrentSearchPage;
22 changes: 22 additions & 0 deletions src/search/reducers/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const initialState = {
query: null,
};

export default function search(state = initialState, action) {
switch (action.type) {
case 'SET_QUERY':
return Object.assign({}, state, {query: action.query});
default:
return state;
}
}

export function getMatchingAddons(addons, query) {
const matches = [];
for (const slug in addons) {
if (addons[slug].title.indexOf(query) >= 0) {
matches.push(addons[slug]);
}
}
return matches;
}
5 changes: 4 additions & 1 deletion test-runner.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
require('babel-polyfill');

const testsContext = require.context('./tests', true, /\.js$/);
const componentsContext = require.context('./src/', true, /components\/.*\.js$/);
const componentsContext = require.context(
'./src/', true, /(actions|components|containers|reducers)\/.*\.js$/);

testsContext.keys().forEach(testsContext);
componentsContext.keys().forEach(componentsContext);
17 changes: 17 additions & 0 deletions tests/core/reducers/test_addons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import addons from 'core/reducers/addons';

describe('addon reducer', () => {
it('returns the old state', () => {
const originalState = {foo: {slug: 'foo'}, bar: {slug: 'bar'}};
assert.strictEqual(originalState, addons(originalState, {type: 'BLAH'}));
});

it('adds an addon', () => {
const originalState = {foo: {slug: 'foo'}};
const baz = {slug: 'baz'};
const expectedState = {foo: {slug: 'foo'}, baz};
const newState = addons(originalState, {type: 'ADDON_FETCHED', addon: baz});
assert.notStrictEqual(originalState, newState);
assert.deepEqual(expectedState, newState);
});
});
5 changes: 2 additions & 3 deletions tests/search/components/TestSearchForm.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import ReactTestUtils from 'react-addons-test-utils';
import { renderIntoDocument as render } from 'react-addons-test-utils';
import { Simulate, renderIntoDocument as render } from 'react-addons-test-utils';

import SearchForm from 'search/components/SearchForm';

Expand Down Expand Up @@ -29,7 +28,7 @@ describe('<SearchForm />', () => {

it('calls onSearch with a search query', () => {
input.value = 'adblock';
ReactTestUtils.Simulate.submit(form);
Simulate.submit(form);
assert.ok(onSearch.calledWith('adblock'));
});
});
49 changes: 49 additions & 0 deletions tests/search/components/TestSearchPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { createRenderer } from 'react-addons-test-utils';

import SearchPage from 'search/components/SearchPage';
import SearchResults from 'search/components/SearchResults';
import SearchForm from 'search/components/SearchForm';

function findByTag(root, tag) {
const matches = root.props.children.filter((child) => child.type === tag);
assert.equal(matches.length, 1, 'expected one match');
return matches[0];
}

describe('<SearchPage />', () => {
let root;
let state;

beforeEach(() => {
state = {
handleSearch: sinon.spy(),
results: [{title: 'Foo', slug: 'foo'}, {title: 'Bar', slug: 'bar'}],
query: 'foo',
};
// eslint-disable-next-line new-cap
const renderer = createRenderer();
renderer.render(<SearchPage {...state} />);
root = renderer.getRenderOutput();
});

it('has a nice heading', () => {
const heading = findByTag(root, 'h1');
assert.equal(heading.props.children, 'Add-on Search');
});

it('renders the results', () => {
const results = findByTag(root, SearchResults);
assert.strictEqual(results.props.results, state.results);
assert.strictEqual(results.props.query, state.query);
assert.deepEqual(
Object.keys(results.props).sort(),
['results', 'query'].sort());
});

it('renders the query', () => {
const form = findByTag(root, SearchForm);
assert.strictEqual(form.props.onSearch, state.handleSearch);
assert.deepEqual(Object.keys(form.props), ['onSearch']);
});
});
17 changes: 17 additions & 0 deletions tests/search/containers/TestCurrentSearchPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { mapStateToProps } from 'search/containers/CurrentSearchPage';

describe('CurrentSearchPage.mapStateToProps', () => {
const props = mapStateToProps({
addons: {ab: {slug: 'ab', title: 'ad-block'},
cd: {slug: 'cd', title: 'cd-block'}},
search: {query: 'ad-block'},
});

it('passes the query through', () => {
assert.equal(props.query, 'ad-block');
});

it('filters the add-ons', () => {
assert.deepEqual(props.results, [{slug: 'ab', title: 'ad-block'}]);
});
});
25 changes: 25 additions & 0 deletions tests/search/reducers/test_search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import search, { getMatchingAddons } from 'search/reducers/search';

describe('search reducer', () => {
it('defaults to a null query', () => {
const { query } = search(undefined, {type: 'unrelated'});
assert.strictEqual(query, null);
});

it('sets the query on SET_QUERY', () => {
const state = search(undefined, {type: 'SET_QUERY', query: 'foo'});
assert.equal(state.query, 'foo');
const newState = search(state, {type: 'SET_QUERY', query: 'bar'});
assert.equal(newState.query, 'bar');
});
});

describe('getMatchingAddons', () => {
it('matches on the title', () => {
assert.deepEqual(
getMatchingAddons(
[{title: 'Foo'}, {title: 'Bar'}, {title: 'Bar Food'}],
'Foo'),
[{title: 'Foo'}, {title: 'Bar Food'}]);
});
});

0 comments on commit 6f033db

Please sign in to comment.