Skip to content
This repository has been archived by the owner on Oct 26, 2021. It is now read-only.

Commit

Permalink
Use redux to manage state
Browse files Browse the repository at this point in the history
  • Loading branch information
kobanyan committed Mar 17, 2018
1 parent f7348df commit c2f29df
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 107 deletions.
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
],
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0"
"react-dom": "^16.2.0",
"react-redux": "^5.0.7",
"redux": "^3.7.2",
"redux-persist": "^5.9.1",
"typescript-fsa": "^2.5.0",
"typescript-fsa-reducers": "^0.4.5"
},
"devDependencies": {
"@types/chrome": "^0.0.60",
Expand All @@ -36,7 +41,9 @@
"@types/puppeteer": "^1.1.0",
"@types/react": "^16.0.40",
"@types/react-dom": "^16.0.4",
"@types/react-redux": "^5.0.15",
"@types/react-test-renderer": "^16.0.1",
"@types/redux": "^3.6.0",
"@types/rimraf": "^2.0.2",
"@types/shelljs": "^0.7.8",
"@types/sinon": "^4.3.0",
Expand Down
135 changes: 60 additions & 75 deletions src/options/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,67 @@
import * as React from 'react';
import { Action } from 'redux';
import { connect, Dispatch } from 'react-redux';

export interface OptionsState {
favoriteColor: string;
likesColor: boolean;
status: string;
import { RootState } from './store';
import { State, updateFavoriteColor, updateLikesColor } from './module';

interface OptionsProps {
values: State;
actions: ActionDispatcher;
}

export class Options extends React.Component<{}, OptionsState> {
constructor(props: {}) {
super(props);
this.state = {
favoriteColor: 'red',
likesColor: false,
status: '',
};
}
componentDidMount() {
chrome.storage.sync.get(
{
favoriteColor: 'red',
likesColor: false,
},
items => {
this.setState({
favoriteColor: items.favoriteColor,
likesColor: items.likesColor,
});
}
);
}
changeFavoriteColor(event: React.ChangeEvent<HTMLSelectElement>) {
this.setState({
favoriteColor: event.currentTarget.value,
});
}
changeLikesColor(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({
likesColor: event.currentTarget.checked,
});
}
save(event: React.MouseEvent<HTMLButtonElement>) {
chrome.storage.sync.set(
{
favoriteColor: this.state.favoriteColor,
likesColor: this.state.likesColor,
},
() => {
// Update status to let user know options were saved.
this.setState({
status: chrome.i18n.getMessage('optionsStatus'),
});
setTimeout(() => {
this.setState({
status: '',
});
}, 750);
}
);
class ActionDispatcher {
constructor(private dispatch: (action: Action) => void) {}
updateFavoriteColor(favoriteColor: string) {
this.dispatch(updateFavoriteColor(favoriteColor));
}
render() {
return (
<section>
{chrome.i18n.getMessage('optionsFavoriteColor')}
<select id="color" value={this.state.favoriteColor} onChange={e => this.changeFavoriteColor(e)}>
<option value="red">{chrome.i18n.getMessage('optionsColorsRed')}</option>
<option value="green">{chrome.i18n.getMessage('optionsColorsGreen')}</option>
<option value="blue">{chrome.i18n.getMessage('optionsColorsBlue')}</option>
<option value="yellow">{chrome.i18n.getMessage('optionsColorsYellow')}</option>
</select>
<label>
<input type="checkbox" id="like" checked={this.state.likesColor} onChange={e => this.changeLikesColor(e)} />
{chrome.i18n.getMessage('optionsLikesColor')}
</label>
<div id="status">{this.state.status}</div>
<button id="save" onClick={e => this.save(e)}>
{chrome.i18n.getMessage('optionsSave')}
</button>
</section>
);
updateLikesColor(likesColor: boolean) {
this.dispatch(updateLikesColor(likesColor));
}
}

const FavoriteColor: React.SFC<OptionsProps> = props => {
return (
<>
{chrome.i18n.getMessage('optionsFavoriteColor')}
<select
id="color"
value={props.values.favoriteColor}
onChange={e => props.actions.updateFavoriteColor(e.currentTarget.value)}
>
<option value="red">{chrome.i18n.getMessage('optionsColorsRed')}</option>
<option value="green">{chrome.i18n.getMessage('optionsColorsGreen')}</option>
<option value="blue">{chrome.i18n.getMessage('optionsColorsBlue')}</option>
<option value="yellow">{chrome.i18n.getMessage('optionsColorsYellow')}</option>
</select>
</>
);
};

const LikesColor: React.SFC<OptionsProps> = props => {
return (
<label>
<input
type="checkbox"
id="like"
checked={props.values.likesColor}
onChange={e => props.actions.updateLikesColor(e.currentTarget.checked)}
/>
{chrome.i18n.getMessage('optionsLikesColor')}
</label>
);
};

const Options: React.SFC<OptionsProps> = props => {
return (
<section>
<FavoriteColor {...props} />
<LikesColor {...props} />
</section>
);
};

export const options = connect(
(state: RootState) => ({ values: state.options }),
(dispatch: Dispatch<Action>) => ({ actions: new ActionDispatcher(dispatch) })
)(Options);
26 changes: 16 additions & 10 deletions src/options/__tests__/Options.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import * as React from 'react';
import { Options } from '../Options';
import { Provider } from 'react-redux';
import * as renderer from 'react-test-renderer';
import { configure } from 'enzyme';
import { configure, mount } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import { mount } from 'enzyme';
const chrome = require('sinon-chrome');
const I18nPlugin = require('sinon-chrome/plugins').I18nPlugin;
import { options as Options } from '../Options';
import { INITIAL_STATE } from '../module';
import { store } from '../store';

const connectedOptions = () => {
return (
<Provider store={store}>
<Options />
</Provider>
);
};

describe('Options', () => {
beforeAll(() => {
Expand All @@ -29,24 +39,20 @@ describe('Options', () => {
delete global.chrome;
});
it('should match snapshot', () => {
const component = renderer.create(<Options />);
const component = renderer.create(connectedOptions());
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('should select', () => {
const options = mount(<Options />);
const options = mount(connectedOptions());
options.find('select').simulate('change', { target: { value: 'green' } });
// https://github.com/airbnb/enzyme/issues/218
// expect(options.state().favoriteColor).toEqual('green');
});
it('should check', () => {
const options = mount(<Options />);
const options = mount(connectedOptions());
options.find('input').simulate('change', { target: { value: true } });
// https://github.com/airbnb/enzyme/issues/218
// expect(options.state().likesColor).toEqual(true);
});
it('should save', () => {
const options = mount(<Options />);
options.find('button').simulate('click');
});
});
11 changes: 0 additions & 11 deletions src/options/__tests__/__snapshots__/Options.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,5 @@ exports[`Options should match snapshot 1`] = `
/>
mocked: optionsLikesColor
</label>
<div
id="status"
>
</div>
<button
id="save"
onClick={[Function]}
>
mocked: optionsSave
</button>
</section>
`;
20 changes: 20 additions & 0 deletions src/options/__tests__/module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createStore } from 'redux';
import * as module from '../module';

let store;

describe('options module', () => {
beforeEach(() => {
store = createStore(module.reducer);
});
it('should update favoriteColor only', () => {
expect(store.getState()).toEqual(module.INITIAL_STATE);
store.dispatch(module.updateFavoriteColor('yellow'));
expect(store.getState()).toEqual({ favoriteColor: 'yellow', likesColor: false });
});
it('should update likesColor only', () => {
expect(store.getState()).toEqual(module.INITIAL_STATE);
store.dispatch(module.updateLikesColor(true));
expect(store.getState()).toEqual({ favoriteColor: 'red', likesColor: true });
});
});
7 changes: 0 additions & 7 deletions src/options/__tests__/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,4 @@ describe('options page', () => {
const likeHandle = await like!.getProperty('checked');
expect(await likeHandle.jsonValue()).toEqual(true);
});
// it('should show the message after clicking save', async () => {
// await page.click('#save');
// // https://github.com/GoogleChrome/puppeteer/issues/1229
// const status = await page.waitForSelector('#status', {visible: true});
// const statusHandle = await status.getProperty('text');
// expect(await statusHandle.jsonValue()).toEqual('Options saved.');
// });
});
14 changes: 12 additions & 2 deletions src/options/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';

import { Options } from './Options';
import { store, persistor } from './store';
import { options as Options } from './Options';

ReactDOM.render(<Options />, document.getElementById('app'));
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Options />
</PersistGate>
</Provider>,
document.getElementById('app')
);
21 changes: 21 additions & 0 deletions src/options/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import actionCreatorFactory from 'typescript-fsa';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
const actionCreator = actionCreatorFactory();

export interface State {
favoriteColor: string;
likesColor: boolean;
}

export const INITIAL_STATE: State = {
favoriteColor: 'red',
likesColor: false,
};

export const updateFavoriteColor = actionCreator<string>('UPDATE_FAVORITE_COLOR');
export const updateLikesColor = actionCreator<boolean>('UPDATE_LIKES_COLOR');

export const reducer = reducerWithInitialState(INITIAL_STATE)
.case(updateFavoriteColor, (state, favoriteColor) => ({ ...state, favoriteColor }))
.case(updateLikesColor, (state, likesColor) => ({ ...state, likesColor }))
.build();
24 changes: 24 additions & 0 deletions src/options/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Reducer, createStore, combineReducers } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';

import { reducer as options, State } from './module';

export type RootState = {
options: State;
};

const rootReducer: Reducer<RootState | void> = combineReducers({
options,
});

const persistConfig = {
key: 'root',
storage,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = createStore(persistedReducer);

export const persistor = persistStore(store);
Loading

0 comments on commit c2f29df

Please sign in to comment.