Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions src/search/components/SearchForm.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';

import { fetchAddon } from 'core/api';
import { loadEntities } from 'core/actions';
import { gettext as _ } from 'core/utils';

import 'search/css/SearchForm.scss';
import 'search/css/lib/buttons.scss';

export default class SearchForm extends React.Component {
export class SearchForm extends React.Component {
static propTypes = {
api: PropTypes.object.isRequired,
loadAddon: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
query: PropTypes.string,
}
static contextTypes = {
router: PropTypes.object,
}

goToSearch(query) {
const { pathname } = this.props;
this.context.router.push(`${pathname}?q=${query}`);
}

handleSubmit = (e) => {
e.preventDefault();
const { pathname } = this.props;
this.goToSearch(this.refs.query.value);
}

handleGo = (e) => {
e.preventDefault();
const query = this.refs.query.value;
this.context.router.push(`${pathname}?q=${query}`);
this.props.loadAddon({ api: this.props.api, query })
.then(
(slug) => this.context.router.push(`/search/addons/${slug}`),
() => this.goToSearch(query));
}

render() {
Expand All @@ -27,8 +44,29 @@ export default class SearchForm extends React.Component {
<form ref="form" className="search-form" onSubmit={this.handleSubmit}>
<label className="visually-hidden" htmlFor="q">{_('Search')}</label>
<input ref="query" type="search" name="q" placeholder={_('Search')} defaultValue={query} />
<button className="button" ref="submit" type="submit">{_('Search')}</button>
<button className="button button-middle" ref="submit" type="submit">{_('Search')}</button>
<button className="button button-end button-inverse" ref="go" onClick={this.handleGo}>
{_("I'm Feeling Lucky")}
</button>
</form>
);
}
}

export function mapStateToProps({ api }) {
return { api };
}

export function mapDispatchToProps(dispatch) {
return {
loadAddon({ api, query }) {
return fetchAddon({ slug: query, api })
.then(({ entities, result }) => {
dispatch(loadEntities(entities));
return result;
});
},
};
}

export default connect(mapStateToProps, mapDispatchToProps)(SearchForm);
2 changes: 0 additions & 2 deletions src/search/css/SearchForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
}

.button {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
height: 3rem;
padding: 0 1rem;
}
Expand Down
16 changes: 16 additions & 0 deletions src/search/css/lib/buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ button.button {
&:hover,
&:focus {
background: $button-background-hover-color;
color: white;
}

&:focus {
Expand All @@ -39,6 +40,21 @@ button.button {
}
}

.button.button-middle {
border-radius: 0;
}

.button.button-end {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}

.button.button-inverse {
background: transparent;
border: 1px solid $button-background-color;
color: $button-background-color;
}

button.button::-moz-focus-inner {
border: 0;
padding: 0;
Expand Down
73 changes: 71 additions & 2 deletions tests/client/search/components/TestSearchForm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import React from 'react';
import { Simulate, renderIntoDocument } from 'react-addons-test-utils';

import SearchForm from 'search/components/SearchForm';
import * as actions from 'core/actions';
import * as coreApi from 'core/api';
import { SearchForm, mapDispatchToProps, mapStateToProps } from 'search/components/SearchForm';

const wait = (time) => new Promise((resolve) => setTimeout(resolve, time));

describe('<SearchForm />', () => {
const pathname = '/somewhere';
let api;
let loadAddon;
let router;
let root;
let form;
Expand All @@ -20,12 +26,14 @@ describe('<SearchForm />', () => {
}

render() {
return <SearchForm pathname={pathname} ref="root" />;
return <SearchForm pathname={pathname} api={api} loadAddon={loadAddon} ref="root" />;
}
}

beforeEach(() => {
router = { push: sinon.spy() };
loadAddon = sinon.stub();
api = sinon.stub();
root = renderIntoDocument(<SearchFormWrapper />).refs.root;
form = root.refs.form;
input = root.refs.query;
Expand All @@ -46,4 +54,65 @@ describe('<SearchForm />', () => {
Simulate.submit(form);
assert(router.push.calledWith('/somewhere?q=adblock'));
});

it('looks up the add-on to see if you are lucky', () => {
loadAddon.returns(Promise.resolve('adblock'));
input.value = 'adblock@adblock.com';
Simulate.click(root.refs.go);
assert(loadAddon.calledWith({ api, query: 'adblock@adblock.com' }));
});

it('redirects to the add-on if you are lucky', () => {
loadAddon.returns(Promise.resolve('adblock'));
assert(!router.push.called);
input.value = 'adblock@adblock.com';
Simulate.click(root.refs.go);
return wait(1)
.then(() => assert(router.push.calledWith('/search/addons/adblock')));
});

it('searches if it is not found', () => {
loadAddon.returns(Promise.reject());
input.value = 'adblock@adblock.com';
Simulate.click(root.refs.go);
return wait(1)
.then(() => assert(router.push.calledWith('/somewhere?q=adblock@adblock.com')));
});
});

describe('SearchForm mapStateToProps', () => {
it('passes the api through', () => {
const api = { lang: 'de', token: 'someauthtoken' };
assert.deepEqual(mapStateToProps({ foo: 'bar', api }), { api });
});
});

describe('SearchForm loadAddon', () => {
it('fetches the add-on', () => {
const slug = 'the-slug';
const api = { token: 'foo' };
const dispatch = sinon.stub();
const addon = sinon.stub();
const entities = { [slug]: addon };
const mockApi = sinon.mock(coreApi);
mockApi
.expects('fetchAddon')
.once()
.withArgs({ slug, api })
.returns(Promise.resolve({ entities }));
const action = sinon.stub();
const mockActions = sinon.mock(actions);
mockActions
.expects('loadEntities')
.once()
.withArgs(entities)
.returns(action);
const { loadAddon } = mapDispatchToProps(dispatch);
return loadAddon({ api, query: slug })
.then(() => {
assert(dispatch.calledWith(action));
mockApi.verify();
mockActions.verify();
});
});
});