From 9595fcdd31c3e4168ae3cafec5a68b5925e69daf Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 12 Apr 2023 17:36:37 +0200 Subject: [PATCH 1/5] wip fix functional tests --- x-pack/plugins/aiops/public/hooks/use_data.ts | 52 ++++++++----------- .../apps/aiops/explain_log_rate_spikes.ts | 50 ++++++++++++------ .../test/functional/apps/aiops/test_data.ts | 19 +++++++ x-pack/test/functional/apps/aiops/types.ts | 43 ++++++++++----- .../aiops/explain_log_rate_spikes_page.ts | 33 +++++++++++- 5 files changed, 136 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index 3546cead3edab..82db675a94c4f 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -45,9 +45,6 @@ export const useData = ( } = useAiopsAppContext(); const [lastRefresh, setLastRefresh] = useState(0); - const [fieldStatsRequest, setFieldStatsRequest] = useState< - DocumentStatsSearchStrategyParams | undefined - >(); /** Prepare required params to pass to search strategy **/ const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { @@ -91,12 +88,30 @@ export const useData = ( ]); const _timeBuckets = useTimeBuckets(); - const timefilter = useTimefilter({ timeRangeSelector: selectedDataView?.timeFieldName !== undefined, autoRefreshSelector: true, }); + const fieldStatsRequest: DocumentStatsSearchStrategyParams | undefined = useMemo(() => { + const timefilterActiveBounds = timefilter.getActiveBounds(); + if (timefilterActiveBounds !== undefined) { + _timeBuckets.setInterval('auto'); + _timeBuckets.setBounds(timefilterActiveBounds); + _timeBuckets.setBarTarget(barTarget); + return { + earliest: timefilterActiveBounds.min?.valueOf(), + latest: timefilterActiveBounds.max?.valueOf(), + intervalMs: _timeBuckets.getInterval()?.asMilliseconds(), + index: selectedDataView.getIndexPattern(), + searchQuery, + timeFieldName: selectedDataView.timeFieldName, + runtimeFieldMap: selectedDataView.getRuntimeMappings(), + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastRefresh, searchQuery]); + const overallStatsRequest = useMemo(() => { return fieldStatsRequest ? { @@ -125,25 +140,6 @@ export const useData = ( lastRefresh ); - function updateFieldStatsRequest() { - const timefilterActiveBounds = timefilter.getActiveBounds(); - if (timefilterActiveBounds !== undefined) { - _timeBuckets.setInterval('auto'); - _timeBuckets.setBounds(timefilterActiveBounds); - _timeBuckets.setBarTarget(barTarget); - setFieldStatsRequest({ - earliest: timefilterActiveBounds.min?.valueOf(), - latest: timefilterActiveBounds.max?.valueOf(), - intervalMs: _timeBuckets.getInterval()?.asMilliseconds(), - index: selectedDataView.getIndexPattern(), - searchQuery, - timeFieldName: selectedDataView.timeFieldName, - runtimeFieldMap: selectedDataView.getRuntimeMappings(), - }); - setLastRefresh(Date.now()); - } - } - useEffect(() => { const timefilterUpdateSubscription = merge( timefilter.getAutoRefreshFetch$(), @@ -156,13 +152,13 @@ export const useData = ( refreshInterval: timefilter.getRefreshInterval(), }); } - updateFieldStatsRequest(); + setLastRefresh(Date.now()); }); // This listens just for an initial update of the timefilter to be switched on. const timefilterEnabledSubscription = timefilter.getEnabledUpdated$().subscribe(() => { if (fieldStatsRequest === undefined) { - updateFieldStatsRequest(); + setLastRefresh(Date.now()); } }); @@ -173,12 +169,6 @@ export const useData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Ensure request is updated when search changes - useEffect(() => { - updateFieldStatsRequest(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchString, JSON.stringify(searchQuery)]); - return { documentStats, timefilter, diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index d34169b05a408..92320dad62087 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -10,7 +10,7 @@ import { orderBy } from 'lodash'; import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; -import type { TestData } from './types'; +import { isTestDataExpectedWithSampleProbability, type TestData } from './types'; import { explainLogRateSpikesTestData } from './test_data'; export default function ({ getPageObject, getService }: FtrProviderContext) { @@ -43,9 +43,21 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.assertTimeRangeSelectorSectionExists(); await ml.testExecution.logTestStep(`${testData.suiteTitle} loads data for full time range`); + if (testData.query) { + await aiops.explainLogRateSpikesPage.setQueryInput(testData.query); + } await aiops.explainLogRateSpikesPage.clickUseFullDataButton( testData.expected.totalDocCountFormatted ); + + if (isTestDataExpectedWithSampleProbability(testData.expected)) { + await aiops.explainLogRateSpikesPage.assertSamplingProbability( + testData.expected.sampleProbabilityFormatted + ); + } else { + await aiops.explainLogRateSpikesPage.assertSamplingProbabilityMissing(); + } + await headerPage.waitUntilLoadingHasFinished(); await ml.testExecution.logTestStep( @@ -147,21 +159,24 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesAnalysisGroupsTable.assertSpikeAnalysisTableExists(); - const analysisGroupsTable = - await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); - - expect(orderBy(analysisGroupsTable, 'group')).to.be.eql( - orderBy(testData.expected.analysisGroupsTable, 'group') - ); + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + const analysisGroupsTable = + await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); + expect(orderBy(analysisGroupsTable, 'group')).to.be.eql( + orderBy(testData.expected.analysisGroupsTable, 'group') + ); + } await ml.testExecution.logTestStep('expand table row'); await aiops.explainLogRateSpikesAnalysisGroupsTable.assertExpandRowButtonExists(); await aiops.explainLogRateSpikesAnalysisGroupsTable.expandRow(); - const analysisTable = await aiops.explainLogRateSpikesAnalysisTable.parseAnalysisTable(); - expect(orderBy(analysisTable, ['fieldName', 'fieldValue'])).to.be.eql( - orderBy(testData.expected.analysisTable, ['fieldName', 'fieldValue']) - ); + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + const analysisTable = await aiops.explainLogRateSpikesAnalysisTable.parseAnalysisTable(); + expect(orderBy(analysisTable, ['fieldName', 'fieldValue'])).to.be.eql( + orderBy(testData.expected.analysisTable, ['fieldName', 'fieldValue']) + ); + } // Assert the field selector that allows to costumize grouping await aiops.explainLogRateSpikesPage.assertFieldFilterPopoverButtonExists(false); @@ -182,11 +197,14 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { if (testData.fieldSelectorApplyAvailable) { await aiops.explainLogRateSpikesPage.clickFieldFilterApplyButton(); - const filteredAnalysisGroupsTable = - await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); - expect(orderBy(filteredAnalysisGroupsTable, 'group')).to.be.eql( - orderBy(testData.expected.filteredAnalysisGroupsTable, 'group') - ); + + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + const filteredAnalysisGroupsTable = + await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable(); + expect(orderBy(filteredAnalysisGroupsTable, 'group')).to.be.eql( + orderBy(testData.expected.filteredAnalysisGroupsTable, 'group') + ); + } } }); } diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index 1ccc441618bdb..3b5f4f8d2c669 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -17,6 +17,24 @@ export const farequoteDataViewTestData: TestData = { chartClickCoordinates: [0, 0], fieldSelectorSearch: 'airline', fieldSelectorApplyAvailable: false, + expected: { + totalDocCountFormatted: '86,374', + sampleProbabilityFormatted: '0.5', + fieldSelectorPopover: ['airline', 'custom_field.keyword'], + }, +}; + +export const farequoteDataViewTestDataWithQuery: TestData = { + suiteTitle: 'farequote with spike', + dataGenerator: 'farequote_with_spike', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'ft_farequote', + brushDeviationTargetTimestamp: 1455033600000, + brushIntervalFactor: 1, + chartClickCoordinates: [0, 0], + fieldSelectorSearch: 'airline', + fieldSelectorApplyAvailable: false, + query: 'NOT airline:("SWR" OR "ACA" OR "AWE" OR "BAW" OR "JAL" OR "JBU" OR "JZA" OR "KLM")', expected: { totalDocCountFormatted: '86,374', analysisGroupsTable: [ @@ -105,5 +123,6 @@ export const artificialLogDataViewTestData: TestData = { export const explainLogRateSpikesTestData: TestData[] = [ farequoteDataViewTestData, + farequoteDataViewTestDataWithQuery, artificialLogDataViewTestData, ]; diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index 7a758aa4a65ff..01733a8e1a2af 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -5,6 +5,34 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +interface TestDataExpectedWithSampleProbability { + totalDocCountFormatted: string; + sampleProbabilityFormatted: string; + fieldSelectorPopover: string[]; +} + +export function isTestDataExpectedWithSampleProbability( + arg: unknown +): arg is TestDataExpectedWithSampleProbability { + return isPopulatedObject(arg, ['sampleProbabilityFormatted']); +} + +interface TestDataExpectedWithoutSampleProbability { + totalDocCountFormatted: string; + analysisGroupsTable: Array<{ group: string; docCount: string }>; + filteredAnalysisGroupsTable?: Array<{ group: string; docCount: string }>; + analysisTable: Array<{ + fieldName: string; + fieldValue: string; + logRate: string; + pValue: string; + impact: string; + }>; + fieldSelectorPopover: string[]; +} + export interface TestData { suiteTitle: string; dataGenerator: string; @@ -17,17 +45,6 @@ export interface TestData { chartClickCoordinates: [number, number]; fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; - expected: { - totalDocCountFormatted: string; - analysisGroupsTable: Array<{ group: string; docCount: string }>; - filteredAnalysisGroupsTable?: Array<{ group: string; docCount: string }>; - analysisTable: Array<{ - fieldName: string; - fieldValue: string; - logRate: string; - pValue: string; - impact: string; - }>; - fieldSelectorPopover: string[]; - }; + query?: string; + expected: TestDataExpectedWithSampleProbability | TestDataExpectedWithoutSampleProbability; } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 3a921a74ee359..093a04d6377b2 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -9,12 +9,17 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; -export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderContext) { +export function ExplainLogRateSpikesPageProvider({ + getService, + getPageObject, +}: FtrProviderContext) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); const ml = getService('ml'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const common = getPageObject('common'); + const header = getPageObject('header'); return { async assertTimeRangeSelectorSectionExists() { @@ -31,6 +36,32 @@ export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderCont }); }, + async assertSamplingProbability(expectedFormattedSamplingProbability: string) { + await retry.tryForTime(5000, async () => { + const samplingProbability = await testSubjects.getVisibleText('aiopsSamplingProbability'); + expect(samplingProbability).to.eql( + expectedFormattedSamplingProbability, + `Expected total document count to be '${expectedFormattedSamplingProbability}' (got '${samplingProbability}')` + ); + }); + }, + + async setQueryInput(query: string) { + const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); + await aiopsQueryInput.type(query); + await common.pressEnterKey(); + await header.waitUntilLoadingHasFinished(); + const queryBarText = await aiopsQueryInput.getVisibleText(); + expect(queryBarText).to.eql( + query, + `Expected query bar text to be '${query}' (got '${queryBarText}')` + ); + }, + + async assertSamplingProbabilityMissing() { + await testSubjects.missingOrFail('aiopsSamplingProbability'); + }, + async clickUseFullDataButton(expectedFormattedTotalDocCount: string) { await retry.tryForTime(30 * 1000, async () => { await testSubjects.clickWhenNotDisabledWithoutRetry('mlDatePickerButtonUseFullData'); From c2f2d7986c6f7e4e2c3ffb70bb5b2148eeff4118 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 12 Apr 2023 19:29:37 +0200 Subject: [PATCH 2/5] fix url state. fix functional tests --- x-pack/packages/ml/url_state/src/url_state.tsx | 11 +++++++++-- x-pack/test/functional/apps/aiops/test_data.ts | 4 ++-- .../services/aiops/explain_log_rate_spikes_page.ts | 3 +-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/packages/ml/url_state/src/url_state.tsx b/x-pack/packages/ml/url_state/src/url_state.tsx index d643a22bde6e4..15160cc033902 100644 --- a/x-pack/packages/ml/url_state/src/url_state.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.tsx @@ -94,6 +94,12 @@ export const UrlStateProvider: FC = ({ children }) => { const history = useHistory(); const { search: searchString } = useLocation(); + const searchStringRef = useRef(searchString); + + useEffect(() => { + searchStringRef.current = searchString; + }, [searchString]); + const setUrlState: SetUrlState = useCallback( ( accessor: Accessor, @@ -101,7 +107,8 @@ export const UrlStateProvider: FC = ({ children }) => { value?: any, replaceState?: boolean ) => { - const prevSearchString = searchString; + const prevSearchString = searchStringRef.current; + const urlState = parseUrlState(prevSearchString); const parsedQueryString = parse(prevSearchString, { sort: false }); @@ -154,7 +161,7 @@ export const UrlStateProvider: FC = ({ children }) => { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [searchString] + [searchStringRef.current] ); return {children}; diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index 3b5f4f8d2c669..b6d3293aeba81 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -36,7 +36,7 @@ export const farequoteDataViewTestDataWithQuery: TestData = { fieldSelectorApplyAvailable: false, query: 'NOT airline:("SWR" OR "ACA" OR "AWE" OR "BAW" OR "JAL" OR "JBU" OR "JZA" OR "KLM")', expected: { - totalDocCountFormatted: '86,374', + totalDocCountFormatted: '48,799', analysisGroupsTable: [ { docCount: '297', @@ -52,7 +52,7 @@ export const farequoteDataViewTestDataWithQuery: TestData = { fieldName: 'airline', fieldValue: 'AAL', logRate: 'Chart type:bar chart', - pValue: '4.66e-11', + pValue: '1.18e-8', impact: 'High', }, ], diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 093a04d6377b2..3da9ed7c760b7 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -18,7 +18,6 @@ export function ExplainLogRateSpikesPageProvider({ const ml = getService('ml'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); - const common = getPageObject('common'); const header = getPageObject('header'); return { @@ -49,7 +48,7 @@ export function ExplainLogRateSpikesPageProvider({ async setQueryInput(query: string) { const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); await aiopsQueryInput.type(query); - await common.pressEnterKey(); + await aiopsQueryInput.pressKeys(browser.keys.ENTER); await header.waitUntilLoadingHasFinished(); const queryBarText = await aiopsQueryInput.getVisibleText(); expect(queryBarText).to.eql( From 5e4f9a5df1bd65398d2c513ba7d76289c3d80c32 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 13 Apr 2023 17:31:13 +0200 Subject: [PATCH 3/5] fix callback dependencies --- x-pack/packages/ml/url_state/src/url_state.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/ml/url_state/src/url_state.tsx b/x-pack/packages/ml/url_state/src/url_state.tsx index 15160cc033902..caaf8d32350b2 100644 --- a/x-pack/packages/ml/url_state/src/url_state.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.tsx @@ -161,7 +161,7 @@ export const UrlStateProvider: FC = ({ children }) => { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [searchStringRef.current] + [] ); return {children}; From 907db31550ba67a608f54af9274a9e6deae01b0c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 13 Apr 2023 20:23:50 +0200 Subject: [PATCH 4/5] wip fixing jest tests --- .../ml/url_state/src/url_state.test.tsx | 116 +++++++++++++----- 1 file changed, 84 insertions(+), 32 deletions(-) diff --git a/x-pack/packages/ml/url_state/src/url_state.test.tsx b/x-pack/packages/ml/url_state/src/url_state.test.tsx index 734c730dd91ba..0214609468dd8 100644 --- a/x-pack/packages/ml/url_state/src/url_state.test.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.test.tsx @@ -5,29 +5,18 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { useEffect, type FC } from 'react'; import { render, act } from '@testing-library/react'; -import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; +import { MemoryRouter } from 'react-router-dom'; -const mockHistoryPush = jest.fn(); +import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; -jest.mock('react-router-dom', () => ({ - useHistory: () => ({ - push: mockHistoryPush, - }), - useLocation: () => ({ - search: - "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d", - }), -})); +const mockHistoryInitialState = + "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d"; describe('getUrlState', () => { test('properly decode url with _g and _a', () => { - expect( - parseUrlState( - "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!t,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d" - ) - ).toEqual({ + expect(parseUrlState(mockHistoryInitialState)).toEqual({ _a: { mlExplorerFilter: {}, mlExplorerSwimlane: { @@ -46,7 +35,7 @@ describe('getUrlState', () => { }, refreshInterval: { display: 'Off', - pause: true, + pause: false, value: 0, }, time: { @@ -61,29 +50,92 @@ describe('getUrlState', () => { }); describe('useUrlState', () => { - beforeEach(() => { - mockHistoryPush.mockClear(); - }); - - test('pushes a properly encoded search string to history', () => { + it('pushes a properly encoded search string to history', () => { const TestComponent: FC = () => { - const [, setUrlState] = useUrlState('_a'); - return ; + const [appState, setAppState] = useUrlState('_a'); + + useEffect(() => { + setAppState(parseUrlState(mockHistoryInitialState)._a); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + +
{JSON.stringify(appState?.query)}
+ + ); }; - const { getByText } = render( - - - + const { getByText, getByTestId } = render( + + + + + + ); + + expect(getByTestId('appState').innerHTML).toBe( + '{"query_string":{"analyze_wildcard":true,"query":"*"}}' ); act(() => { getByText('ButtonText').click(); }); - expect(mockHistoryPush).toHaveBeenCalledWith({ - search: - '_a=%28mlExplorerFilter%3A%28%29%2CmlExplorerSwimlane%3A%28viewByFieldName%3Aaction%29%2Cquery%3A%28%29%29&_g=%28ml%3A%28jobIds%3A%21%28dec-2%29%29%2CrefreshInterval%3A%28display%3AOff%2Cpause%3A%21f%2Cvalue%3A0%29%2Ctime%3A%28from%3A%272019-01-01T00%3A03%3A40.000Z%27%2Cmode%3Aabsolute%2Cto%3A%272019-08-30T11%3A55%3A07.000Z%27%29%29&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d', + expect(getByTestId('appState').innerHTML).toBe('"my-query"'); + }); + + it('updates both _g and _a state successfully', () => { + const TestComponent: FC = () => { + const [globalState, setGlobalState] = useUrlState('_g'); + const [appState, setAppState] = useUrlState('_a'); + + useEffect(() => { + setGlobalState(parseUrlState(mockHistoryInitialState)._g); + setAppState(parseUrlState(mockHistoryInitialState)._a); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + +
{JSON.stringify(globalState?.time?.to)}
+
{JSON.stringify(appState?.query)}
+ + ); + }; + + const { getByText, getByTestId } = render( + + + + + + ); + + expect(getByTestId('globalState').innerHTML).toBe('"2019-08-30T11:55:07.000Z"'); + expect(getByTestId('appState').innerHTML).toBe( + '{"query_string":{"analyze_wildcard":true,"query":"*"}}' + ); + + act(() => { + getByText('GlobalStateButton1').click(); + // getByText('AppStateButton').click(); + // getByText('GlobalStateButton2').click(); }); + + expect(getByTestId('globalState').innerHTML).toBe('"2019-08-30T11:55:07.000Z"'); + expect(getByTestId('appState').innerHTML).toBe( + '{"query_string":{"analyze_wildcard":true,"query":"*"}}' + ); }); }); From 625dd4f8decf69f4064ccf89993c256531f1f4da Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 14 Apr 2023 09:01:09 +0200 Subject: [PATCH 5/5] fix url state --- .../ml/url_state/src/url_state.test.tsx | 46 ++++++++++--------- .../packages/ml/url_state/src/url_state.tsx | 4 ++ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/x-pack/packages/ml/url_state/src/url_state.test.tsx b/x-pack/packages/ml/url_state/src/url_state.test.tsx index 0214609468dd8..033ecd77fadf4 100644 --- a/x-pack/packages/ml/url_state/src/url_state.test.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.test.tsx @@ -92,24 +92,20 @@ describe('useUrlState', () => { const [appState, setAppState] = useUrlState('_a'); useEffect(() => { - setGlobalState(parseUrlState(mockHistoryInitialState)._g); - setAppState(parseUrlState(mockHistoryInitialState)._a); + setGlobalState({ time: 'initial time' }); + setAppState({ query: 'initial query' }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> - - - + + -
{JSON.stringify(globalState?.time?.to)}
-
{JSON.stringify(appState?.query)}
+
{globalState?.time}
+
{appState?.query}
); }; @@ -122,20 +118,28 @@ describe('useUrlState', () => { ); - expect(getByTestId('globalState').innerHTML).toBe('"2019-08-30T11:55:07.000Z"'); - expect(getByTestId('appState').innerHTML).toBe( - '{"query_string":{"analyze_wildcard":true,"query":"*"}}' - ); + expect(getByTestId('globalState').innerHTML).toBe('initial time'); + expect(getByTestId('appState').innerHTML).toBe('initial query'); act(() => { getByText('GlobalStateButton1').click(); - // getByText('AppStateButton').click(); - // getByText('GlobalStateButton2').click(); }); - expect(getByTestId('globalState').innerHTML).toBe('"2019-08-30T11:55:07.000Z"'); - expect(getByTestId('appState').innerHTML).toBe( - '{"query_string":{"analyze_wildcard":true,"query":"*"}}' - ); + expect(getByTestId('globalState').innerHTML).toBe('now-15m'); + expect(getByTestId('appState').innerHTML).toBe('initial query'); + + act(() => { + getByText('AppStateButton').click(); + }); + + expect(getByTestId('globalState').innerHTML).toBe('now-15m'); + expect(getByTestId('appState').innerHTML).toBe('the updated query'); + + act(() => { + getByText('GlobalStateButton2').click(); + }); + + expect(getByTestId('globalState').innerHTML).toBe('now-5y'); + expect(getByTestId('appState').innerHTML).toBe('the updated query'); }); }); diff --git a/x-pack/packages/ml/url_state/src/url_state.tsx b/x-pack/packages/ml/url_state/src/url_state.tsx index caaf8d32350b2..bd62e1f61029a 100644 --- a/x-pack/packages/ml/url_state/src/url_state.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.tsx @@ -149,6 +149,10 @@ export const UrlStateProvider: FC = ({ children }) => { if (oldLocationSearchString !== newLocationSearchString) { const newSearchString = stringify(parsedQueryString, { sort: false }); + // Another `setUrlState` call could happen before the updated + // `searchString` gets propagated via `useLocation` therefore + // we update the ref right away too. + searchStringRef.current = newSearchString; if (replaceState) { history.replace({ search: newSearchString }); } else {