diff --git a/src/search/components/SearchForm.js b/src/search/components/SearchForm.js index d2f69d6313d..59f8f1c57bc 100644 --- a/src/search/components/SearchForm.js +++ b/src/search/components/SearchForm.js @@ -1,12 +1,17 @@ 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, } @@ -14,11 +19,23 @@ export default class SearchForm extends React.Component { 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() { @@ -27,8 +44,29 @@ export default class SearchForm extends React.Component {
- + +
); } } + +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); diff --git a/src/search/css/SearchForm.scss b/src/search/css/SearchForm.scss index f1771600da8..5151a7202b8 100644 --- a/src/search/css/SearchForm.scss +++ b/src/search/css/SearchForm.scss @@ -14,8 +14,6 @@ } .button { - border-bottom-left-radius: 0; - border-top-left-radius: 0; height: 3rem; padding: 0 1rem; } diff --git a/src/search/css/lib/buttons.scss b/src/search/css/lib/buttons.scss index 8980638a028..44c2fa45cf9 100644 --- a/src/search/css/lib/buttons.scss +++ b/src/search/css/lib/buttons.scss @@ -22,6 +22,7 @@ button.button { &:hover, &:focus { background: $button-background-hover-color; + color: white; } &:focus { @@ -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; diff --git a/tests/client/search/components/TestSearchForm.js b/tests/client/search/components/TestSearchForm.js index 5e601a99a76..e0456f26e7e 100644 --- a/tests/client/search/components/TestSearchForm.js +++ b/tests/client/search/components/TestSearchForm.js @@ -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('', () => { const pathname = '/somewhere'; + let api; + let loadAddon; let router; let root; let form; @@ -20,12 +26,14 @@ describe('', () => { } render() { - return ; + return ; } } beforeEach(() => { router = { push: sinon.spy() }; + loadAddon = sinon.stub(); + api = sinon.stub(); root = renderIntoDocument().refs.root; form = root.refs.form; input = root.refs.query; @@ -46,4 +54,65 @@ describe('', () => { 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(); + }); + }); });