import React from 'react' ; import { of } from 'rxjs' ; import { Provider } from 'react-redux' ; import { createBrowserHistory } from 'history' ; import { renderHook , act , RenderHookResult } from '@testing-library/react-hooks' ; import { DashboardSessionStorage } from '../lib' ; import { coreMock } from '../../../../../core/public/mocks' ; import { DashboardConstants } from '../../dashboard_constants' ; import { dataPluginMock } from '../../../../data/public/mocks' ; import { SavedObjectLoader } from '../../services/saved_objects' ; import { DashboardAppServices , DashboardAppState } from '../../types' ; import { DashboardContainer } from '../embeddable/dashboard_container' ; import { KibanaContextProvider } from '../../../../kibana_react/public' ; import { EmbeddableFactory , ViewMode } from '../../services/embeddable' ; import { dashboardStateStore , setDescription , setViewMode } from '../state' ; import { DashboardContainerServices } from '../embeddable/dashboard_container' ; import { createKbnUrlStateStorage , defer } from '../../../../kibana_utils/public' ; import { Filter , IIndexPattern , IndexPatternsContract } from '../../services/data' ; import { useDashboardAppState , UseDashboardStateProps } from './use_dashboard_app_state' ; import { getSampleDashboardInput , getSavedDashboardMock , makeDefaultServices , } from '../test_helpers' ; interface SetupEmbeddableFactoryReturn { finalizeEmbeddableCreation : ( ) => void ; dashboardContainer : DashboardContainer ; dashboardDestroySpy : jest . SpyInstance < unknown > ; } interface RenderDashboardStateHookReturn { embeddableFactoryResult : SetupEmbeddableFactoryReturn ; renderHookResult : RenderHookResult < Partial < UseDashboardStateProps > , DashboardAppState > ; services : DashboardAppServices ; props : UseDashboardStateProps ; } const originalDashboardEmbeddableId = 'originalDashboardEmbeddableId' ; const createDashboardAppStateProps = ( ) : UseDashboardStateProps => ( { kbnUrlStateStorage : createKbnUrlStateStorage ( ) , savedDashboardId : 'testDashboardId' , history : createBrowserHistory ( ) , isEmbeddedExternally : false , } ) ; const createDashboardAppStateServices = ( ) => { const defaults = makeDefaultServices ( ) ; const indexPatterns = { } as IndexPatternsContract ; const defaultIndexPattern = { id : 'foo' , fields : [ { name : 'bar' } ] } as IIndexPattern ; indexPatterns . ensureDefaultDataView = jest . fn ( ) . mockImplementation ( ( ) => Promise . resolve ( true ) ) ; indexPatterns . getDefault = jest . fn ( ) . mockImplementation ( ( ) => Promise . resolve ( defaultIndexPattern ) ) ; const data = dataPluginMock . createStartContract ( ) ; data . query . filterManager . getUpdates$ = jest . fn ( ) . mockImplementation ( ( ) => of ( void 0 ) ) ; data . query . queryString . getUpdates$ = jest . fn ( ) . mockImplementation ( ( ) => of ( { } ) ) ; data . query . timefilter . timefilter . getTimeUpdate$ = jest . fn ( ) . mockImplementation ( ( ) => of ( void 0 ) ) ; data . query . timefilter . timefilter . getRefreshIntervalUpdate$ = jest . fn ( ) . mockImplementation ( ( ) => of ( void 0 ) ) ; return { ... defaults , indexPatterns , data } ; } ; const setupEmbeddableFactory = ( services , id ) : SetupEmbeddableFactoryReturn => { const coreStart = coreMock . createStart ( ) ; const containerOptions = { notifications : services . core . notifications , savedObjectMetaData : { } as unknown , ExitFullScreenButton : ( ) => null , embeddable : services . embeddable , uiSettings : services . uiSettings , SavedObjectFinder : ( ) => null , overlays : coreStart . overlays , application : { } as unknown , inspector : { } as unknown , uiActions : { } as unknown , http : coreStart . http , } as unknown as DashboardContainerServices ; const dashboardContainer = new DashboardContainer ( { ... getSampleDashboardInput ( ) , id } , containerOptions ) ; const deferEmbeddableCreate = defer ( ) ; services . embeddable . getEmbeddableFactory = jest . fn ( ) . mockImplementation ( ( ) => ( { create : ( ) => deferEmbeddableCreate . promise , } as unknown as EmbeddableFactory ) ) ; const dashboardDestroySpy = jest . spyOn ( dashboardContainer , 'destroy' ) ; return { dashboardContainer , dashboardDestroySpy , finalizeEmbeddableCreation : ( ) => { act ( ( ) => { deferEmbeddableCreate . resolve ( dashboardContainer ) ; } ) ; } , } ; } ; const renderDashboardAppStateHook = ( { partialProps , partialServices , } ) : RenderDashboardStateHookReturn => { const props = { ... createDashboardAppStateProps ( ) , ... ( partialProps ?? { } ) } ; const services = { ... createDashboardAppStateServices ( ) , ... ( partialServices ?? { } ) } ; const embeddableFactoryResult = setupEmbeddableFactory ( services , originalDashboardEmbeddableId ) ; const renderHookResult = renderHook ( ( replaceProps ) => useDashboardAppState ( { ... props , ... replaceProps } ) , { wrapper : ( { children } ) => ( < Provider store = { dashboardStateStore } > < KibanaContextProvider services = { services } > { children } < /KibanaContextProvider> < /Provider>), } ) ; return { embeddableFactoryResult , renderHookResult , services , props } ; } ; describe ( 'Dashboard container lifecycle' , ( ) => { test ( 'Dashboard container is destroyed on unmount' , async ( ) => { const { renderHookResult , embeddableFactoryResult } = renderDashboardAppStateHook ( { } ) ; embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( embeddableFactoryResult . dashboardContainer ) . toBe ( renderHookResult . result . current . dashboardContainer ) ; expect ( embeddableFactoryResult . dashboardDestroySpy ) . not . toBeCalled ( ) ; renderHookResult . unmount ( ) ; expect ( embeddableFactoryResult . dashboardDestroySpy ) . toBeCalled ( ) ; } ) ; test ( 'Old dashboard container is destroyed when new dashboardId is given' , async ( ) => { const { renderHookResult , embeddableFactoryResult , services } = renderDashboardAppStateHook ( { } ) ; const getResult = ( ) => renderHookResult . result . current ; expect ( getResult ( ) . dashboardContainer ) . toBeUndefined ( ) ; embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( embeddableFactoryResult . dashboardContainer ) . toBe ( getResult ( ) . dashboardContainer ) ; expect ( embeddableFactoryResult . dashboardDestroySpy ) . not . toBeCalled ( ) ; const newDashboardId = 'wow_a_new_dashboard_id' ; const embeddableFactoryNew = setupEmbeddableFactory ( services , newDashboardId ) ; renderHookResult . rerender ( { savedDashboardId : newDashboardId } ) ; embeddableFactoryNew . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( embeddableFactoryNew . dashboardContainer ) . toEqual ( getResult ( ) . dashboardContainer ) ; expect ( embeddableFactoryNew . dashboardDestroySpy ) . not . toBeCalled ( ) ; expect ( embeddableFactoryResult . dashboardDestroySpy ) . toBeCalled ( ) ; } ) ; test ( 'Dashboard container is destroyed if dashboard id is changed before container is resolved' , async ( ) => { const { renderHookResult , embeddableFactoryResult , services } = renderDashboardAppStateHook ( { } ) ; const getResult = ( ) => renderHookResult . result . current ; expect ( getResult ( ) . dashboardContainer ) . toBeUndefined ( ) ; await act ( ( ) => Promise . resolve ( ) ) ; const newDashboardId = 'wow_a_new_dashboard_id' ; const embeddableFactoryNew = setupEmbeddableFactory ( services , newDashboardId ) ; renderHookResult . rerender ( { savedDashboardId : newDashboardId } ) ; await act ( ( ) => Promise . resolve ( ) ) ; embeddableFactoryNew . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( embeddableFactoryNew . dashboardContainer ) . toBe ( getResult ( ) . dashboardContainer ) ; expect ( embeddableFactoryNew . dashboardDestroySpy ) . not . toBeCalled ( ) ; embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await act ( ( ) => Promise . resolve ( ) ) ; expect ( embeddableFactoryNew . dashboardContainer ) . toBe ( getResult ( ) . dashboardContainer ) ; expect ( embeddableFactoryNew . dashboardDestroySpy ) . not . toBeCalled ( ) ; expect ( embeddableFactoryResult . dashboardDestroySpy ) . toBeCalled ( ) ; } ) ; } ) ; describe . skip ( 'Dashboard initial state' , ( ) => { it ( 'Extracts state from Dashboard Saved Object' , async ( ) => { const { renderHookResult , embeddableFactoryResult } = renderDashboardAppStateHook ( { } ) ; const getResult = ( ) => renderHookResult . result . current ; expect ( getResult ( ) . savedDashboard ) . toBeUndefined ( ) ; embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( getResult ( ) . savedDashboard ) . toBeDefined ( ) ; expect ( getResult ( ) . savedDashboard ?. title ) . toEqual ( getResult ( ) . getLatestDashboardState ?. ( ) . title ) ; } ) ; it ( 'Sets initial time range and filters from saved dashboard' , async ( ) => { const savedDashboards = { } as SavedObjectLoader ; savedDashboards . get = jest . fn ( ) . mockImplementation ( ( id ? ) => Promise . resolve ( getSavedDashboardMock ( { getFilters : ( ) => [ { meta : { test : 'filterMeTimbers' } } as unknown as Filter ] , timeRestore : true , timeFrom : 'now-13d' , timeTo : 'now' , id , } ) ) ) ; const partialServices = { savedDashboards } ; const { renderHookResult , embeddableFactoryResult , services } = renderDashboardAppStateHook ( { partialServices , } ) ; const getResult = ( ) => renderHookResult . result . current ; embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( getResult ( ) . getLatestDashboardState ?. ( ) . timeRestore ) . toEqual ( true ) ; expect ( services . data . query . timefilter . timefilter . setTime ) . toHaveBeenCalledWith ( { from : 'now-13d' , to : 'now' , } ) ; expect ( services . data . query . filterManager . setAppFilters ) . toHaveBeenCalledWith ( [ { meta : { test : 'filterMeTimbers' } } as unknown as Filter , ] ) ; } ) ; it ( 'Combines session state and URL state into initial state' , async ( ) => { const dashboardSessionStorage = { getState : jest . fn ( ) . mockReturnValue ( { viewMode : ViewMode . EDIT , description : 'this should be overwritten' } ) , } as unknown as DashboardSessionStorage ; const kbnUrlStateStorage = createKbnUrlStateStorage ( ) ; kbnUrlStateStorage . set ( '_a' , { description : 'with this' } ) ; const { renderHookResult , embeddableFactoryResult } = renderDashboardAppStateHook ( { partialProps : { kbnUrlStateStorage } , partialServices : { dashboardSessionStorage } , } ) ; const getResult = ( ) => renderHookResult . result . current ; embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await renderHookResult . waitForNextUpdate ( ) ; expect ( getResult ( ) . getLatestDashboardState ?. ( ) . description ) . toEqual ( 'with this' ) ; expect ( getResult ( ) . getLatestDashboardState ?. ( ) . viewMode ) . toEqual ( ViewMode . EDIT ) ; } ) ; } ) ; describe . skip ( 'Dashboard state sync' , ( ) => { let defaultDashboardAppStateHookResult ; const getResult = ( ) => defaultDashboardAppStateHookResult . renderHookResult . result . current ; beforeEach ( async ( ) => { DashboardConstants . CHANGE_APPLY_DEBOUNCE = 0 ; DashboardConstants . CHANGE_CHECK_DEBOUNCE = 0 ; defaultDashboardAppStateHookResult = renderDashboardAppStateHook ( { } ) ; defaultDashboardAppStateHookResult . embeddableFactoryResult . finalizeEmbeddableCreation ( ) ; await defaultDashboardAppStateHookResult . renderHookResult . waitForNextUpdate ( ) ; } ) ; it ( 'Updates Dashboard container input when state changes' , async ( ) => { const { embeddableFactoryResult } = defaultDashboardAppStateHookResult ; embeddableFactoryResult . dashboardContainer . updateInput = jest . fn ( ) ; act ( ( ) => { dashboardStateStore . dispatch ( setDescription ( 'Well hello there new description' ) ) ; } ) ; await act ( async ( ) => { await new Promise ( ( resolve ) => setTimeout ( resolve , 3 ) ) ; } ) ; expect ( embeddableFactoryResult . dashboardContainer . updateInput ) . toHaveBeenCalledWith ( expect . objectContaining ( { description : 'Well hello there new description' } ) ) ; } ) ; it ( 'Updates state when dashboard container input changes' , async ( ) => { const { embeddableFactoryResult } = defaultDashboardAppStateHookResult ; expect ( getResult ( ) . getLatestDashboardState ?. ( ) . fullScreenMode ) . toBe ( false ) ; act ( ( ) => { embeddableFactoryResult . dashboardContainer . updateInput ( { isFullScreenMode : true , } ) ; } ) ; await act ( ( ) => Promise . resolve ( ) ) ; expect ( getResult ( ) . getLatestDashboardState ?. ( ) . fullScreenMode ) . toBe ( true ) ; } ) ; it ( 'pushes unsaved changes to the session storage' , async ( ) => { const { services } = defaultDashboardAppStateHookResult ; expect ( getResult ( ) . getLatestDashboardState ?. ( ) . fullScreenMode ) . toBe ( false ) ; act ( ( ) => { dashboardStateStore . dispatch ( setViewMode ( ViewMode . EDIT ) ) ; dashboardStateStore . dispatch ( setDescription ( 'Wow an even cooler description.' ) ) ; } ) ; await act ( async ( ) => { await new Promise ( ( resolve ) => setTimeout ( resolve , 3 ) ) ; } ) ; expect ( services . dashboardSessionStorage . setState ) . toHaveBeenCalledWith ( 'testDashboardId' , expect . objectContaining ( { description : 'Wow an even cooler description.' , viewMode : ViewMode . EDIT , } ) ) ; } ) ; } ) ;