Skip to content

Commit

Permalink
InputFilter tests and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
GregoryPotdevin committed Mar 21, 2016
1 parent 7f7c86e commit 5f10fa9
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 22 deletions.
123 changes: 103 additions & 20 deletions src/components/search/filters/input-filter/InputFilter.tsx
@@ -1,6 +1,7 @@
import * as React from "react";

import {
QueryAccessor,
SearchkitComponent,
SearchkitComponentProps,
ReactComponentType
Expand All @@ -13,6 +14,8 @@ import {
} from "../../../ui"

const defaults = require('lodash/defaults')
const throttle = require("lodash/throttle")
const assign = require("lodash/assign")

export interface InputFilterProps extends SearchkitComponentProps {
id: string
Expand All @@ -29,45 +32,125 @@ export interface InputFilterProps extends SearchkitComponentProps {
}

export class InputFilter extends SearchkitComponent<InputFilterProps, any> {
accessor:QueryAccessor
lastSearchMs:number
throttledSearch: () => void

static translations:any = {
"searchbox.placeholder":"Search"
}
translations = SearchBox.translations

static defaultProps = {
containerComponent: Panel,
collapsable: false,
mod: "sk-input-filter",
searchThrottleTime:200
}

static propTypes = defaults({
id: React.PropTypes.string.isRequired,
id:React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
searchOnChange: React.PropTypes.bool,
searchThrottleTime: React.PropTypes.number,
searchOnChange:React.PropTypes.bool,
searchThrottleTime:React.PropTypes.number,
queryFields:React.PropTypes.arrayOf(React.PropTypes.string),
prefixQueryFields:React.PropTypes.arrayOf(React.PropTypes.string),
queryOptions:React.PropTypes.object,
collapsable: React.PropTypes.bool,
prefixQueryFields:React.PropTypes.arrayOf(React.PropTypes.string),
translations:SearchkitComponent.translationsPropType(
SearchBox.translations
),
mod: React.PropTypes.string,
placeholder: React.PropTypes.string
}, SearchkitComponent.propTypes)

static defaultProps = {
containerComponent: Panel,
collapsable: false,
mod: "sk-input-filter"
constructor (props:InputFilterProps) {
super(props);
this.state = {
focused:false
}
this.lastSearchMs = 0
this.throttledSearch = throttle(()=> {
this.searchQuery(this.accessor.getQueryString())
}, props.searchThrottleTime)
}

componentWillMount() {
super.componentWillMount()
}

defineBEMBlocks() {
return { container:this.props.mod };
}

defineAccessor(){
const { id, title, prefixQueryFields, queryFields, searchOnChange, queryOptions } = this.props
return new QueryAccessor(id, {
title,
addToFilters: true,
prefixQueryFields:(searchOnChange ? (prefixQueryFields || queryFields) : false),
queryFields:queryFields || ["_all"],
queryOptions:assign({}, queryOptions)
})
}

onSubmit(event) {
event.preventDefault()
this.searchQuery(this.getValue())
}

searchQuery(query) {
let shouldResetOtherState = false
this.accessor.setQueryString(query, shouldResetOtherState )
let now = +new Date
let newSearch = now - this.lastSearchMs <= 2000
this.lastSearchMs = now
this.searchkit.performSearch(newSearch)
}

getValue(){
return (this.accessor.state.getValue() || "") + ""
}

onChange(e){
const query = e.target.value;
this.accessor.setQueryString(query)
if (this.props.searchOnChange) {
this.throttledSearch()
}
this.forceUpdate()
}

setFocusState(focused:boolean) {
this.setState({focused:focused})
}

render() {
const { containerComponent, title, id, collapsable } = this.props

let block = this.bemBlocks.container

return React.createElement(containerComponent, {
title,
className: id ? `filter--${id}` : undefined,
disabled: false,
disabled: (this.searchkit.getHitsCount() == 0) && (this.getValue() == ""),
collapsable
},
<SearchBox id={id}
mod={this.props.mod}
queryFields={this.props.queryFields}
prefixQueryFields={this.props.prefixQueryFields}
queryOptions={this.props.queryOptions}
searchOnChange={this.props.searchOnChange}
searchThrottleTime={this.props.searchThrottleTime}
placeholder={this.props.placeholder}
autofocus={false} />
<div className={block().state({focused:this.state.focused})}>
<form onSubmit={this.onSubmit.bind(this)}>
<div className={block("icon")}></div>
<input type="text"
data-qa="input-filter"
className={block("text")}
placeholder={this.props.placeholder || this.translate("searchbox.placeholder")}
value={this.getValue()}
onFocus={this.setFocusState.bind(this, true)}
onBlur={this.setFocusState.bind(this, false)}
ref="queryField"
autoFocus={false}
onInput={this.onChange.bind(this)}/>
<input type="submit" value="search" className={block("action")} data-qa="submit"/>
<div data-qa="loader" className={block("loader").mix("sk-spinning-loader").state({hidden:!this.isLoading()})}></div>
</form>
</div>
);
}

Expand Down
199 changes: 199 additions & 0 deletions src/components/search/filters/input-filter/InputFilter.unit.tsx
@@ -0,0 +1,199 @@
import * as React from "react";
import {mount} from "enzyme";
import { InputFilter } from "./InputFilter";
import { SearchkitManager } from "../../../../core";
const bem = require("bem-cn");
import {
fastClick, hasClass, jsxToHTML, printPrettyHtml
} from "../../../__test__/TestHelpers"

import * as sinon from "sinon";

describe("Searchbox tests", () => {

beforeEach(() => {

this.searchkit = SearchkitManager.mock()

this.searchkit.translateFunction = (key)=> {
return {
"searchbox.placeholder":"search placeholder",
}[key]
}

this.createWrapper = (searchOnChange=false, queryFields=null, prefixQueryFields=null, otherProps={}) => {
this.wrapper = mount(
<InputFilter searchkit={this.searchkit}
id="test_id"
title="Test title"
searchOnChange={searchOnChange}
queryFields={queryFields}
prefixQueryFields={prefixQueryFields}
{...otherProps} />
);
this.accessor = this.searchkit.accessors.getAccessors()[0]
}

this.setResults = ()=> {
this.searchkit.setResults({
hits:{
hits:[{_id:1, title:1},{_id:2,title:2}],
total:2
}
})
}

this.setEmptyResults = () => {
this.searchkit.setResults({
hits: {
total: 0
}
})
}

this.typeSearch = (value)=> {
this.wrapper.find(".sk-input-filter__text")
.simulate("input", {target:{value}})
}

});

it("render", () => {
this.createWrapper()
expect(this.wrapper.find(".sk-input-filter__text").get(0).placeholder).toBe("search placeholder")
})

it("toggles visibility", () => {
let spy = sinon.spy()
this.searchkit.performSearch = spy
this.createWrapper(true)

this.setEmptyResults()
expect(hasClass(this.wrapper.find(".sk-panel"), "is-disabled")).toBe(true)

this.setResults()
expect(hasClass(this.wrapper.find(".sk-panel"), "is-disabled")).toBe(false)

// Don't hide if active filter
this.typeSearch("noresults")
this.setEmptyResults()
expect(hasClass(this.wrapper.find(".sk-panel"), "is-disabled")).toBe(false)
})

it("should allow custom mod and className", () => {
this.createWrapper(false, null, null, {
mod: "my-input", className: "my-class"
})
this.setResults()
expect(this.wrapper.html()).toEqual(jsxToHTML(
<div className="sk-panel filter--test_id">
<div className="sk-panel__header">Test title</div>
<div className="sk-panel__content">
<div className="my-input">
<form>
<div className="my-input__icon" />
<input type="text" data-qa="input-filter" className="my-input__text" placeholder="search placeholder" value=""/>
<input type="submit" value="search" className="my-input__action" data-qa="submit" />
<div data-qa="loader" className="my-input__loader sk-spinning-loader is-hidden"></div>
</form>
</div>
</div>
</div>
))
})

it("search on change", () => {
let spy = sinon.spy()
this.searchkit.performSearch = spy
this.createWrapper(true)
this.typeSearch("m")
expect(this.accessor.state.getValue()).toBe("m")
expect(spy.callCount).toBe(1)
this.typeSearch("ma")
expect(this.accessor.state.getValue()).toBe("ma")
expect(spy.callCount).toBe(1) // throttling should block it
this.wrapper.node.throttledSearch.flush()
expect(spy.callCount).toBe(2)
})

it("search on change with clock", ()=> {
jasmine.clock().install()
let queries = []
this.searchkit.performSearch = ()=> {
queries.push(this.searchkit.buildQuery())
}
expect(this.wrapper.node.props.searchThrottleTime).toBe(200)
this.createWrapper(true)
this.typeSearch("m")
jasmine.clock().tick(100)
expect(queries.length).toBe(1)
expect(queries[0].getQueryString()).toBe("m")
this.typeSearch("ma")
jasmine.clock().tick(100)
expect(queries.length).toBe(1)
jasmine.clock().tick(300)
expect(queries.length).toBe(2)
expect(queries[1].getQueryString()).toBe("ma")
jasmine.clock().uninstall()
})

it("search on submit", () => {
let spy = sinon.spy()
this.searchkit.performSearch = spy

this.createWrapper(false)
this.typeSearch('m')
this.typeSearch('ma')
expect(this.accessor.state.getValue()).toBe("ma")
expect(spy.callCount).toBe(0)
this.wrapper.find("form").simulate("submit")
expect(spy.callCount).toBe(1)
})

it("should configure accessor defaults correctly", ()=> {
this.createWrapper(false, ["title"])

expect(this.accessor.key).toBe("test_id")
let options = this.accessor.options
expect(options).toEqual({
title: "Test title",
addToFilters: true,
"queryFields": ["title"],
prefixQueryFields:false,
"queryOptions": {}
})

})

it("should configure accessor search on change correctly", ()=> {
this.createWrapper(true, ["title"])

expect(this.accessor.key).toBe("test_id")
let options = this.accessor.options
expect(options).toEqual({
title: "Test title",
addToFilters: true,
queryFields: ["title"],
prefixQueryFields:["title"],
queryOptions: {}
})

})

it("should configure accessor + prefix", ()=> {
this.createWrapper(true, ["title"], ["prefix"])

expect(this.accessor.key).toBe("test_id")
let options = this.accessor.options
expect(options).toEqual({
title: "Test title",
addToFilters: true,
"queryFields": ["title"],
prefixQueryFields:["prefix"],
"queryOptions": {}
})

})


});
15 changes: 14 additions & 1 deletion src/core/accessors/QueryAccessor.ts
Expand Up @@ -11,6 +11,8 @@ export interface SearchOptions {
queryFields?:Array<string>
prefixQueryFields?:Array<string>
queryOptions?:any
title?: string
addToFilters?:boolean
}
export class QueryAccessor extends BaseQueryAccessor {
options:SearchOptions
Expand Down Expand Up @@ -40,8 +42,19 @@ export class QueryAccessor extends BaseQueryAccessor {
fields:this.options.prefixQueryFields
}))
}
return query.addQuery(BoolShould(queries))
query = query.addQuery(BoolShould(queries))
.setQueryString(queryStr)

if (this.options.addToFilters){
query = query.addSelectedFilter({
name: this.options.title,
value: queryStr,
id: this.key,
remove: () => this.state = this.state.clear()
})
}

return query
}
return query

Expand Down

0 comments on commit 5f10fa9

Please sign in to comment.