import { assert } from "chai" ; import { mount , ReactWrapper } from "enzyme" ; import * as React from "react" ; import * as sinon from "sinon" ; import { InputGroup , IPopoverProps , Keys , MenuItem , Popover } from "@blueprintjs/core" ; import { IFilm , renderFilm , TOP_100_FILMS } from "../../docs-app/src/common/films" ; import { IItemRendererProps , QueryList } from "../src" ; import { ISuggestProps , ISuggestState , Suggest } from "../src/components/select/suggest" ; import { selectComponentSuite } from "./selectComponentSuite" ; describe ( "Suggest" , ( ) => { const FilmSuggest = Suggest . ofType < IFilm > ( ) ; const defaultProps = { items : TOP_100_FILMS , popoverProps : { isOpen : true , usePortal : false } , query : "" , } ; let handlers ; beforeEach ( ( ) => { handlers = { inputValueRenderer : sinon . spy ( inputValueRenderer ) , itemPredicate : sinon . spy ( filterByYear ) , itemRenderer : sinon . spy ( renderFilm ) , onItemSelect : sinon . spy ( ) , } ; } ) ; selectComponentSuite < ISuggestProps < IFilm > , ISuggestState < IFilm > > ( props => mount ( < Suggest { ... props } inputValueRenderer = { inputValueRenderer } popoverProps = { { isOpen : true , usePortal : false } } />)); describe ( "Basic behavior" , ( ) => { it ( "renders an input that triggers a popover containing items" , ( ) => { const wrapper = suggest ( ) ; const popover = wrapper . find ( Popover ) ; assert . lengthOf ( wrapper . find ( InputGroup ) , 1 , "should render InputGroup" ) ; assert . lengthOf ( popover , 1 , "should render Popover" ) ; assert . lengthOf ( popover . find ( MenuItem ) , 100 , "should render 100 items in popover" ) ; } ) ; describe ( "when ESCAPE key pressed" , ( ) => { runEscTabKeyDownTests ( Keys . ESCAPE ) ; } ) ; describe ( "when TAB key pressed" , ( ) => { runEscTabKeyDownTests ( Keys . TAB ) ; } ) ; it ( "does not open popover on BACKSPACE, ARROW_LEFT, or ARROW_RIGHT" , ( ) => { const wrapper = suggest ( { openOnKeyDown : true , popoverProps : { usePortal : false } } ) ; simulateFocus ( wrapper ) ; checkKeyDownDoesNotOpenPopover ( wrapper , Keys . BACKSPACE ) ; checkKeyDownDoesNotOpenPopover ( wrapper , Keys . ARROW_LEFT ) ; checkKeyDownDoesNotOpenPopover ( wrapper , Keys . ARROW_RIGHT ) ; } ) ; it ( "opens popover if any other key pressed" , ( ) => { const wrapper = suggest ( { openOnKeyDown : true } ) ; simulateKeyDown ( wrapper , Keys . SPACE ) ; assert . isTrue ( wrapper . state ( ) . isOpen , "should open popover" ) ; } ) ; it ( "scrolls active item into view when popover opens" , ( ) => { const wrapper = suggest ( ) ; const queryList = ( ( wrapper . instance ( ) as Suggest < IFilm > ) as any ) . queryList ; const scrollActiveItemIntoViewSpy = sinon . spy ( queryList , "scrollActiveItemIntoView" ) ; wrapper . setState ( { isOpen : false } ) ; assert . isFalse ( scrollActiveItemIntoViewSpy . called ) ; wrapper . setState ( { isOpen : true } ) ; assert . strictEqual ( scrollActiveItemIntoViewSpy . callCount , 1 , "should call scrollActiveItemIntoView" ) ; } ) ; it ( "sets active item to the selected item when the popover is closed" , done => { const wrapper = suggest ( { popoverProps : { transitionDuration : 5 } , selectedItem : TOP_100_FILMS [ 10 ] , } ) ; const queryList = ( ( wrapper . instance ( ) as Suggest < IFilm > ) as any ) . queryList as QueryList < IFilm > ; assert . deepEqual ( queryList . state . activeItem , wrapper . state ( ) . selectedItem , "QueryList activeItem should be set to the controlled selectedItem if prop is provided" ) ; simulateFocus ( wrapper ) ; assert . isTrue ( wrapper . state ( ) . isOpen ) ; const newActiveItem = TOP_100_FILMS [ 11 ] ; queryList . setActiveItem ( newActiveItem ) ; assert . deepEqual ( queryList . state . activeItem , newActiveItem ) ; simulateKeyDown ( wrapper , Keys . ESCAPE ) ; assert . isFalse ( wrapper . state ( ) . isOpen ) ; wrapper . update ( ) ; wrapper . find ( QueryList ) . update ( ) ; setTimeout ( ( ) => { assert . deepEqual ( queryList . state . activeItem , wrapper . state ( ) . selectedItem ) ; done ( ) ; } , 10 ) ; } ) ; function checkKeyDownDoesNotOpenPopover ( wrapper , which ) { simulateKeyDown ( wrapper , which ) ; assert . isFalse ( wrapper . state ( ) . isOpen , "should not open popover" ) ; } function runEscTabKeyDownTests ( which ) { it ( "closes popover" , ( ) => { const wrapper = suggest ( ) ; simulateFocus ( wrapper ) ; simulateKeyDown ( wrapper , which ) ; assert . isFalse ( wrapper . state ( ) . isOpen , "should close popover" ) ; } ) ; it ( "preserves currently selected item" , ( ) => { const ITEM_INDEX = 4 ; const expectedItem = TOP_100_FILMS [ ITEM_INDEX ] ; const wrapper = suggest ( { closeOnSelect : false } ) ; simulateFocus ( wrapper ) ; selectItem ( wrapper , ITEM_INDEX ) ; simulateKeyDown ( wrapper , which ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , expectedItem , "before typing" ) ; simulateChange ( wrapper , "new query" ) ; simulateKeyDown ( wrapper , which ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , expectedItem , "after typing" ) ; } ) ; } } ) ; describe ( "closeOnSelect" , ( ) => { it ( "clicking an item closes the popover if closeOnSelect=true" , ( ) => { const ITEM_INDEX = 4 ; const wrapper = suggest ( ) ; selectItem ( wrapper , ITEM_INDEX ) ; assert . isFalse ( wrapper . state ( ) . isOpen ) ; } ) ; it ( "clicking an item does not close the popover if closeOnSelect=false" , ( ) => { const ITEM_INDEX = 4 ; const wrapper = suggest ( { closeOnSelect : false } ) ; selectItem ( wrapper , ITEM_INDEX ) ; assert . isTrue ( wrapper . state ( ) . isOpen ) ; } ) ; } ) ; describe ( "inputProps" , ( ) => { it ( "value and onChange are ignored" , ( ) => { const value = "nailed it" ; const onChange = sinon . spy ( ) ; const input = suggest ( { inputProps : { value , onChange } } ) . find ( "input" ) ; assert . notStrictEqual ( input . prop ( "onChange" ) , onChange ) ; assert . notStrictEqual ( input . prop ( "value" ) , value ) ; } ) ; it ( "invokes inputProps key handlers" , ( ) => { const spy = sinon . spy ( ) ; const wrapper = suggest ( { inputProps : { onKeyDown : spy , onKeyUp : spy } } ) ; simulateKeyDown ( wrapper ) ; simulateKeyUp ( wrapper ) ; assert . strictEqual ( spy . callCount , 2 ) ; } ) ; } ) ; describe ( "inputValueRenderer" , ( ) => { it ( "invokes inputValueRenderer when rendering an item in the input field" , ( ) => { const ITEM_INDEX = 4 ; const wrapper = suggest ( ) ; assert . isFalse ( handlers . inputValueRenderer . called , "should not call inputValueRenderer before selection" ) ; selectItem ( wrapper , ITEM_INDEX ) ; const selectedItem = TOP_100_FILMS [ ITEM_INDEX ] ; const expectedValue = inputValueRenderer ( selectedItem ) ; assert . isTrue ( handlers . inputValueRenderer . called , "should call inputValueRenderer after selection" ) ; assert . strictEqual ( wrapper . find ( InputGroup ) . prop ( "value" ) , expectedValue ) ; } ) ; } ) ; describe ( "openOnKeyDown" , ( ) => { it ( "opens the popover on key down if openOnKeyDown=true" , ( ) => { const wrapper = suggest ( { openOnKeyDown : true } ) ; simulateKeyDown ( wrapper ) ; assert . isTrue ( wrapper . state ( ) . isOpen , "popover should open on key down" ) ; } ) ; it ( "opens the popover on focus if openOnKeyDown=false" , ( ) => { const wrapper = suggest ( ) ; simulateFocus ( wrapper ) ; assert . isTrue ( wrapper . state ( ) . isOpen , "popover should open on focus" ) ; simulateKeyDown ( wrapper ) ; assert . isTrue ( wrapper . state ( ) . isOpen , "popover should stay open on key down" ) ; } ) ; } ) ; describe ( "popoverProps" , ( ) => { const onOpening = sinon . spy ( ) ; afterEach ( ( ) => { onOpening . resetHistory ( ) ; } ) ; it ( "popover can be controlled with popoverProps" , ( ) => { const modifiers = { } ; const wrapper = suggest ( { popoverProps : getPopoverProps ( false , modifiers ) } ) ; wrapper . setProps ( { popoverProps : getPopoverProps ( true , modifiers ) } ) . update ( ) ; assert . strictEqual ( wrapper . find ( Popover ) . prop ( "modifiers" ) , modifiers ) ; assert . isTrue ( onOpening . calledOnce ) ; } ) ; function getPopoverProps ( isOpen , modifiers ) { return { ... defaultProps . popoverProps , isOpen , modifiers , onOpening , } ; } } ) ; describe ( "Uncontrolled Mode with default value" , ( ) => { it ( "initialize the selectedItem with the defaultSelectedItem" , ( ) => { const defaultSelectedItem = TOP_100_FILMS [ 0 ] ; const wrapper = suggest ( { defaultSelectedItem } ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , defaultSelectedItem , "The selected item should be initialized" ) ; } ) ; it ( "when a new item is selected, it changes the selectedItem" , ( ) => { const ITEM_INDEX = 4 ; const defaultSelectedItem = TOP_100_FILMS [ 0 ] ; const nextSelectedItem = TOP_100_FILMS [ ITEM_INDEX ] ; const wrapper = suggest ( { defaultSelectedItem } ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , defaultSelectedItem , "The selected item should be initialized" ) ; simulateFocus ( wrapper ) ; selectItem ( wrapper , ITEM_INDEX ) ; assert . isTrue ( handlers . onItemSelect . called , "onItemSelect should be called after selection" ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , nextSelectedItem , "the selectedItem should be updated" ) ; } ) ; } ) ; describe ( "Controlled Mode" , ( ) => { it ( "initialize the selectedItem with the given value" , ( ) => { const selectedItem = TOP_100_FILMS [ 0 ] ; assert . isNotNull ( selectedItem , "The selected item we test must not be null" ) ; const wrapper = suggest ( { selectedItem } ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , selectedItem ) ; } ) ; it ( "propagates the selectedItem with new values" , ( ) => { const selectedItem = TOP_100_FILMS [ 0 ] ; assert . isNotNull ( selectedItem , "The selected item we test must not be null" ) ; const wrapper = suggest ( ) ; assert . isNull ( wrapper . state ( ) . selectedItem ) ; wrapper . setProps ( { selectedItem } ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , selectedItem ) ; } ) ; it ( "when new item selected, it should respect the selectedItem prop" , ( ) => { const selectedItem = TOP_100_FILMS [ 0 ] ; const ITEM_INDEX = 4 ; assert . isNotNull ( selectedItem , "The selected item we test must not be null" ) ; const wrapper = suggest ( { selectedItem } ) ; simulateFocus ( wrapper ) ; selectItem ( wrapper , ITEM_INDEX ) ; assert . isTrue ( handlers . onItemSelect . called , "onItemSelect should be called after selection" ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , selectedItem , "the underlying state should not change" ) ; const newSelectedItem = TOP_100_FILMS [ ITEM_INDEX ] ; wrapper . setProps ( { selectedItem : newSelectedItem } ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , newSelectedItem , "the selectedItem should be updated" ) ; } ) ; it ( "preserves the empty selection" , ( ) => { const ITEM_INDEX = 4 ; const selectedItem = TOP_100_FILMS [ 0 ] ; const wrapper = suggest ( { selectedItem : null } ) ; assert . isNull ( wrapper . state ( ) . selectedItem ) ; simulateFocus ( wrapper ) ; selectItem ( wrapper , ITEM_INDEX ) ; assert . isTrue ( handlers . onItemSelect . called , "onItemSelect should be called after selection" ) ; assert . isNull ( wrapper . state ( ) . selectedItem , "the underlying state should not change" ) ; wrapper . setProps ( { selectedItem } ) ; assert . strictEqual ( wrapper . state ( ) . selectedItem , selectedItem , "the selectedItem should be updated" ) ; } ) ; } ) ; function suggest ( props = { } , query ? ) { const wrapper = mount < typeof FilmSuggest > ( < FilmSuggest { ... defaultProps } { ... handlers } { ... props } / > ) ; if ( query !== undefined ) { wrapper . setState ( { query } ) ; } return wrapper ; } } ) ; function filterByYear ( query , film ) { return query === "" || film . year . toString ( ) === query ; } function selectItem ( wrapper , index ) { wrapper . find ( "a" ) . at ( index ) . simulate ( "click" ) ; } function inputValueRenderer ( item ) { return item . title ; } function simulateChange ( wrapper , value ) { wrapper . find ( "input" ) . simulate ( "change" , { target : { value } } ) ; } function simulateFocus ( wrapper ) { wrapper . find ( "input" ) . simulate ( "focus" ) ; } function simulateKeyDown ( wrapper , which = Keys . SPACE ) { wrapper . find ( "input" ) . simulate ( "keydown" , { which } ) ; } function simulateKeyUp ( wrapper , which = Keys . SPACE ) { wrapper . find ( "input" ) . simulate ( "keyup" , { which } ) ; }