import { assert } from "chai" ; import { mount , ReactWrapper , shallow } from "enzyme" ; import * as React from "react" ; import * as sinon from "sinon" ; import { Menu } from "@blueprintjs/core" ; import { IQueryListProps } from "@blueprintjs/select" ; import { IFilm , renderFilm , TOP_100_FILMS } from "../../docs-app/src/common/films" ; import { IQueryListRendererProps , IQueryListState , ItemListPredicate , ItemListRenderer , ItemPredicate , QueryList , } from "../src" ; type FilmQueryListWrapper = ReactWrapper < IQueryListProps < IFilm > , IQueryListState < IFilm > > ; describe ( "<QueryList>" , ( ) => { const FilmQueryList = QueryList . ofType < IFilm > ( ) ; const testProps = { itemRenderer : sinon . spy ( renderFilm ) , items : TOP_100_FILMS . slice ( 0 , 20 ) , onActiveItemChange : sinon . spy ( ) , onItemSelect : sinon . spy ( ) , renderer : sinon . spy ( ( props ) => < div > { props . itemList } < /div>), } ; beforeEach ( ( ) => { testProps . itemRenderer . resetHistory ( ) ; testProps . onActiveItemChange . resetHistory ( ) ; testProps . onItemSelect . resetHistory ( ) ; testProps . renderer . resetHistory ( ) ; } ) ; describe ( "items" , ( ) => { it ( "handles controlled changes to the whole items list" , ( ) => { const wrapper = shallow ( < FilmQueryList { ... testProps } / > ) ; const newItems = TOP_100_FILMS . slice ( 0 , 1 ) ; wrapper . setProps ( { items : newItems } ) ; assert . deepEqual ( wrapper . state ( "filteredItems" ) , newItems ) ; } ) ; } ) ; describe ( "itemListRenderer" , ( ) => { const itemListRenderer = props => ( < ul className = "foo" > { props . items . map ( props . renderItem ) } < /ul>); it ( "renderItem calls itemRenderer" , ( ) => { const wrapper = shallow ( < FilmQueryList { ... testProps } itemListRenderer = { itemListRenderer } / > ) ; assert . lengthOf ( wrapper . find ( "ul.foo" ) , 1 , "should find element" ) ; assert . equal ( testProps . itemRenderer . callCount , 20 ) ; } ) ; } ) ; describe ( "filtering" , ( ) => { it ( "itemPredicate filters each item by query" , ( ) => { const predicate = sinon . spy ( ( query , film ) => film . year === + query ) ; shallow ( < FilmQueryList { ... testProps } itemPredicate = { predicate } query = "1994" / > ) ; assert . equal ( predicate . callCount , testProps . items . length , "called once per item" ) ; const { filteredItems } = testProps . renderer . args [ 0 ] [ 0 ] as IQueryListRendererProps < IFilm > ; assert . lengthOf ( filteredItems , 3 , "returns only films from 1994" ) ; } ) ; it ( "itemListPredicate filters entire list by query" , ( ) => { const predicate = sinon . spy ( ( query , films ) => films . filter ( f => f . year === + query ) ) ; shallow ( < FilmQueryList { ... testProps } itemListPredicate = { predicate } query = "1994" / > ) ; assert . equal ( predicate . callCount , 1 , "called once for entire list" ) ; const { filteredItems } = testProps . renderer . args [ 0 ] [ 0 ] as IQueryListRendererProps < IFilm > ; assert . lengthOf ( filteredItems , 3 , "returns only films from 1994" ) ; } ) ; it ( "prefers itemListPredicate if both are defined" , ( ) => { const predicate = sinon . spy ( ( ) => true ) ; const listPredicate = ( _q , items ) => items ; const listPredicateSpy = sinon . spy ( listPredicate ) ; shallow ( < FilmQueryList { ... testProps } itemPredicate = { predicate } itemListPredicate = { listPredicateSpy } query = "1980" / > ) ; assert . isTrue ( listPredicateSpy . called , "listPredicate should be invoked" ) ; assert . isFalse ( predicate . called , "item predicate should not be invoked" ) ; } ) ; it ( "omitting both predicate props is supported" , ( ) => { shallow ( < FilmQueryList { ... testProps } query = "1980" / > ) ; const { filteredItems } = testProps . renderer . args [ 0 ] [ 0 ] as IQueryListRendererProps < IFilm > ; assert . lengthOf ( filteredItems , testProps . items . length , "returns all films" ) ; } ) ; it ( "ensure onActiveItemChange is not called with undefined and empty list" , ( ) => { const myItem = { title : "Toy Story 3" , year : 2010 , rank : 1 } ; const filmQueryList = mount ( < FilmQueryList { ... testProps } items = { [ myItem ] } activeItem = { myItem } query = "" / > ) ; filmQueryList . setState ( { query : "query" } ) ; filmQueryList . setState ( { activeItem : undefined } ) ; assert . equal ( testProps . onActiveItemChange . callCount , 0 ) ; } ) ; it ( "ensure onActiveItemChange is not called updating props and query doesn't change" , ( ) => { const myItem = { title : "Toy Story 3" , year : 2010 , rank : 1 } ; const props = { ... testProps , activeItem : myItem , items : [ myItem ] , query : "" , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; filmQueryList . setProps ( props ) ; assert . equal ( testProps . onActiveItemChange . callCount , 0 ) ; } ) ; it ( "ensure activeItem changes on query change" , ( ) => { const props = { ... testProps , items : [ TOP_100_FILMS [ 0 ] ] , query : "abc" , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , TOP_100_FILMS [ 0 ] ) ; filmQueryList . setProps ( { items : [ TOP_100_FILMS [ 1 ] ] , query : "123" , } ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , TOP_100_FILMS [ 1 ] ) ; } ) ; it ( "ensure activeItem changes on when no longer in new items" , ( ) => { const props = { ... testProps , items : [ TOP_100_FILMS [ 0 ] ] , query : "abc" , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , TOP_100_FILMS [ 0 ] ) ; filmQueryList . setProps ( { items : [ TOP_100_FILMS [ 1 ] ] , } ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , TOP_100_FILMS [ 1 ] ) ; } ) ; } ) ; describe ( "activeItem state initialization" , ( ) => { it ( "initializes to first filtered item when uncontrolled" , ( ) => { const props = { ... testProps , itemPredicate : ( _query , item ) => item === TOP_100_FILMS [ 11 ] , query : "123" , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; assert ( filmQueryList . state ( ) . activeItem === TOP_100_FILMS [ 11 ] ) ; } ) ; it ( "initializes to controlled activeItem prop (non-null)" , ( ) => { const props = { ... testProps , activeItem : TOP_100_FILMS [ 11 ] , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; assert ( filmQueryList . state ( ) . activeItem === TOP_100_FILMS [ 11 ] ) ; } ) ; it ( "initializes to controlled activeItem prop (null)" , ( ) => { const props = { ... testProps , activeItem : null , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; assert ( filmQueryList . state ( ) . activeItem === null ) ; } ) ; it ( "createNewItemPosition affects position of create new item" , ( ) => { const props = { ... testProps , createNewItemFromQuery : sinon . spy ( ) , createNewItemRenderer : ( ) => < article />, items : TOP_100_FILMS . slice ( 0 , 4 ) , query : "the" , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; assert ( filmQueryList . find ( Menu ) . children ( ) . children ( ) . last ( ) . is ( "article" ) ) ; filmQueryList . setProps ( { createNewItemPosition : "first" } ) ; assert ( filmQueryList . find ( Menu ) . children ( ) . children ( ) . first ( ) . is ( "article" ) ) ; } ) ; } ) ; describe ( "scrolling" , ( ) => { it ( "brings active item into view" ) ; } ) ; describe ( "pasting" , ( ) => { const onItemsPaste = sinon . spy ( ) ; const itemPredicate = ( query , film , _i ? , exactMatch ? ) => { return exactMatch === true ? query . toLowerCase ( ) === film . title . toLowerCase ( ) : true ; } ; function mountForPasteTest ( overrideProps = { } ) { let handlePaste ; const props = { ... testProps , itemPredicate , onItemsPaste , renderer : sinon . spy ( ( listItemsProps ) => { handlePaste = listItemsProps . handlePaste ; return testProps . renderer ( listItemsProps ) ; } ) , ... overrideProps , } ; const filmQueryList = mount ( < FilmQueryList { ... props } / > ) ; return { filmQueryList , handlePaste : handlePaste ! } ; } afterEach ( ( ) => { onItemsPaste . resetHistory ( ) ; } ) ; it ( "converts 1 pasted value into an item" , ( ) => { const { filmQueryList , handlePaste } = mountForPasteTest ( ) ; const pastedValue = TOP_100_FILMS [ 0 ] . title ; handlePaste ( [ pastedValue ] ) ; assert . isTrue ( onItemsPaste . calledOnce ) ; assert . deepEqual ( onItemsPaste . args [ 0 ] [ 0 ] , [ TOP_100_FILMS [ 0 ] ] ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , TOP_100_FILMS [ 0 ] ) ; assert . deepEqual ( filmQueryList . state ( ) . query , "" ) ; } ) ; it ( "convert multiple pasted values into items" , ( ) => { const { filmQueryList , handlePaste } = mountForPasteTest ( ) ; const item1 = TOP_100_FILMS [ 6 ] ; const item2 = TOP_100_FILMS [ 0 ] ; const item3 = TOP_100_FILMS [ 3 ] ; const pastedValue1 = item1 . title ; const pastedValue2 = item2 . title ; const pastedValue3 = item3 . title ; handlePaste ( [ pastedValue1 , pastedValue2 , pastedValue3 ] ) ; assert . isTrue ( onItemsPaste . calledOnce ) ; assert . deepEqual ( onItemsPaste . args [ 0 ] [ 0 ] , [ item1 , item2 , item3 ] ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , item3 ) ; assert . deepEqual ( filmQueryList . state ( ) . query , "" ) ; } ) ; it ( "concatenates unrecognized values into the ghost input by default" , ( ) => { const { filmQueryList , handlePaste } = mountForPasteTest ( ) ; const item2 = TOP_100_FILMS [ 6 ] ; const item4 = TOP_100_FILMS [ 3 ] ; const pastedValue1 = "unrecognized1" ; const pastedValue2 = item2 . title ; const pastedValue3 = "unrecognized2" ; const pastedValue4 = item4 . title ; handlePaste ( [ pastedValue1 , pastedValue2 , pastedValue3 , pastedValue4 ] ) ; assert . isTrue ( onItemsPaste . calledOnce ) ; assert . deepEqual ( onItemsPaste . args [ 0 ] [ 0 ] , [ item2 , item4 ] ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , item4 ) ; assert . deepEqual ( filmQueryList . state ( ) . query , "unrecognized1, unrecognized2" ) ; } ) ; it ( "creates new items out of unrecognized values if 'Create item' option is enabled" , ( ) => { const createdRank = 0 ; const createdYear = 2019 ; const { filmQueryList , handlePaste } = mountForPasteTest ( { createNewItemFromQuery : query => ( { rank : createdRank , title : query , year : createdYear , } ) , createNewItemRenderer : ( ) => < div > Create item < / div > , } ) ; const item1 = TOP_100_FILMS [ 6 ] ; const item2 = TOP_100_FILMS [ 3 ] ; const pastedValue1 = item1 . title ; const pastedValue2 = item2 . title ; const pastedValue3 = "unrecognized" ; handlePaste ( [ pastedValue1 , pastedValue2 , pastedValue3 ] ) ; const createdItem = { title : "unrecognized" , rank : createdRank , year : createdYear } ; assert . isTrue ( onItemsPaste . calledOnce ) ; assert . deepEqual ( onItemsPaste . args [ 0 ] [ 0 ] , [ item1 , item2 , createdItem ] ) ; assert . deepEqual ( filmQueryList . state ( ) . activeItem , item2 ) ; assert . deepEqual ( filmQueryList . state ( ) . query , "" ) ; } ) ; } ) ; describe ( "query" , ( ) => { it ( "trims leading and trailing whitespace when creating new items" , ( ) => { let triggerInputQueryChange ; const createNewItemFromQuerySpy = sinon . spy ( ) ; const createNewItemRendererSpy = sinon . spy ( ) ; const renderer = sinon . spy ( ( props ) => { triggerInputQueryChange = props . handleQueryChange ; return < div > { props . itemList } < /div>; } ) ; shallow ( < FilmQueryList { ... testProps } renderer = { renderer } createNewItemFromQuery = { createNewItemFromQuerySpy } createNewItemRenderer = { createNewItemRendererSpy } / > ) ; const untrimmedQuery = " foo " ; const trimmedQuery = untrimmedQuery . trim ( ) ; assert . isDefined ( triggerInputQueryChange , "query list should render with input change callbacks" ) ; triggerInputQueryChange ! ( { target : { value : untrimmedQuery } } ) ; assert . isTrue ( createNewItemFromQuerySpy . calledWith ( trimmedQuery ) ) ; assert . isTrue ( createNewItemRendererSpy . calledWith ( trimmedQuery ) ) ; } ) ; it ( "resets the query after creating new item if resetOnSelect=true" , ( ) => { const onQueryChangeSpy = runResetOnSelectTest ( true ) ; assert . isTrue ( onQueryChangeSpy . calledWith ( "" ) ) ; } ) ; it ( "does not reset the query after creating new item if resetOnSelect=false" , ( ) => { const onQueryChangeSpy = runResetOnSelectTest ( false ) ; assert . isTrue ( onQueryChangeSpy . notCalled ) ; } ) ; function runResetOnSelectTest ( resetOnSelect ) { let triggerItemCreate ; const onQueryChangeSpy = sinon . spy ( ) ; const createNewItemRenderer = sinon . spy ( ( _query , _active , handleClick ) => { triggerItemCreate = handleClick ; return < div />; } ) ; const queryList = shallow ( < FilmQueryList { ... testProps } createNewItemFromQuery = { ( ) => ( { title : "irrelevant" , rank : 0 , year : 0 } ) } createNewItemRenderer = { createNewItemRenderer } onQueryChange = { onQueryChangeSpy } resetOnSelect = { resetOnSelect } / > ) ; ( queryList . instance ( ) as QueryList < IFilm > ) . setQuery ( "some query" ) ; onQueryChangeSpy . resetHistory ( ) ; assert . isDefined ( triggerItemCreate , "query list should pass click handler to createNewItemRenderer" ) ; triggerItemCreate ! ( { } ) ; return onQueryChangeSpy ; } } ) ; } ) ;