Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Defend Workflows] Fix bug when event filter value cannot be changed without using {backspace} #192196

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ describe('AutocompleteFieldMatchComponent', () => {
.fn()
.mockResolvedValue([false, true, ['value 3', 'value 4'], jest.fn()]);

const findEuiComboBox = () =>
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
onSearchChange: (a: string) => void;
onCreateOption: (a: string) => void;
};

beforeEach(() => {
(useFieldValueAutocomplete as jest.Mock).mockReturnValue([
false,
Expand Down Expand Up @@ -171,7 +178,7 @@ describe('AutocompleteFieldMatchComponent', () => {
).toEqual('127.0.0.1');
});

test('it invokes "onChange" when new value created', async () => {
test('it invokes "onChange" when new value created', () => {
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchComponent
Expand All @@ -192,16 +199,12 @@ describe('AutocompleteFieldMatchComponent', () => {
/>
);

(
wrapper.find(EuiComboBox).props() as unknown as {
onCreateOption: (a: string) => void;
}
).onCreateOption('127.0.0.1');
findEuiComboBox().onCreateOption('127.0.0.1');

expect(mockOnChange).toHaveBeenCalledWith('127.0.0.1');
});

test('it invokes "onChange" when new value selected', async () => {
test('it invokes "onChange" when new value selected', () => {
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchComponent
Expand All @@ -222,15 +225,39 @@ describe('AutocompleteFieldMatchComponent', () => {
/>
);

(
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([{ label: 'value 1' }]);
findEuiComboBox().onChange([{ label: 'value 1' }]);

expect(mockOnChange).toHaveBeenCalledWith('value 1');
});

test('it invokes "onChange" with empty value (i.e. clears selection) when new value searched', () => {
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={mockOnChange}
onError={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue="value 1"
/>
);

act(() => {
findEuiComboBox().onSearchChange('value 12');
});

expect(mockOnChange).toHaveBeenCalledWith('');
});

test('should show the warning helper text if the new value contains spaces when change', async () => {
(useFieldValueAutocomplete as jest.Mock).mockReturnValue([
false,
Expand Down Expand Up @@ -259,13 +286,7 @@ describe('AutocompleteFieldMatchComponent', () => {
/>
);

await waitFor(() =>
(
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([{ label: ' value 1 ' }])
);
await waitFor(() => findEuiComboBox().onChange([{ label: ' value 1 ' }]));
wrapper.update();
expect(mockOnChange).toHaveBeenCalledWith(' value 1 ');

Expand Down Expand Up @@ -293,11 +314,7 @@ describe('AutocompleteFieldMatchComponent', () => {
/>
);
act(() => {
(
wrapper.find(EuiComboBox).props() as unknown as {
onSearchChange: (a: string) => void;
}
).onSearchChange('value 1');
findEuiComboBox().onSearchChange('value 1');
});

expect(useFieldValueAutocomplete).toHaveBeenCalledWith({
Expand All @@ -313,6 +330,54 @@ describe('AutocompleteFieldMatchComponent', () => {
selectedField: getField('machine.os.raw'),
});
});

test('it refreshes autocomplete with search query when input field is cleared', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue="windows"
/>
);

act(() => {
findEuiComboBox().onSearchChange('value 1');
});
act(() => {
findEuiComboBox().onSearchChange('');
});

// 1st call is initial render, 2nd call sets the search query:
expect(useFieldValueAutocomplete).toHaveBeenNthCalledWith(2, {
autocompleteService: autocompleteStartMock,
fieldValue: 'windows',
indexPattern: { fields, id: '1234', title: 'logstash-*' },
operatorType: 'match',
query: 'value 1',
selectedField: getField('machine.os.raw'),
});
// last call is the refresh when input field is cleared
expect(useFieldValueAutocomplete).toHaveBeenLastCalledWith({
autocompleteService: autocompleteStartMock,
fieldValue: 'windows',
indexPattern: { fields, id: '1234', title: 'logstash-*' },
operatorType: 'match',
query: '',
selectedField: getField('machine.os.raw'),
});
});

test('should show the warning helper text if the new value contains spaces when searching a new query', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
Expand All @@ -333,18 +398,15 @@ describe('AutocompleteFieldMatchComponent', () => {
/>
);
act(() => {
(
wrapper.find(EuiComboBox).props() as unknown as {
onSearchChange: (a: string) => void;
}
).onSearchChange(' value 1');
findEuiComboBox().onSearchChange(' value 1');
});

wrapper.update();
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});

test('should show the warning helper text if selectedValue contains spaces when editing', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
Expand All @@ -368,6 +430,7 @@ describe('AutocompleteFieldMatchComponent', () => {
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});

test('should not show the warning helper text if selectedValue is falsy', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,26 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
handleWarning(warning);

if (!err) handleSpacesWarning(searchVal);
setSearchQuery(searchVal);
}

if (searchVal) {
// Clear selected option when user types to allow user to modify value without {backspace}
onChange('');
}

// Update search query unconditionally to show correct suggestions even when input is cleared
setSearchQuery(searchVal);
},
[handleError, handleSpacesWarning, isRequired, selectedField, touched, handleWarning, warning]
[
selectedField,
onChange,
isRequired,
touched,
handleError,
handleWarning,
warning,
handleSpacesWarning,
]
);

const handleCreateOption = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { APP_EVENT_FILTERS_PATH } from '../../../../../common/constants';
import type { ArtifactsFixtureType } from '../../fixtures/artifacts_page';
import { getArtifactsListTestsData } from '../../fixtures/artifacts_page';
import {
createArtifactList,
createPerPolicyArtifact,
removeAllArtifacts,
} from '../../tasks/artifacts';
import { loadPage } from '../../tasks/common';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { login } from '../../tasks/login';
import type { ReturnTypeFromChainable } from '../../types';

describe('Event Filters', { tags: ['@ess', '@serverless'] }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;

const CONDITION_VALUE = 'valuesAutocompleteMatch';
const SUBMIT_BUTTON = 'EventFiltersListPage-flyout-submitButton';

before(() => {
indexEndpointHosts().then((indexEndpoints) => {
endpointData = indexEndpoints;
});
});

after(() => {
removeAllArtifacts();

endpointData?.cleanup();
endpointData = undefined;
});

beforeEach(() => {
removeAllArtifacts();
});

describe('when editing event filter value', () => {
let eventFiltersMock: ArtifactsFixtureType;
beforeEach(() => {
login();

eventFiltersMock = getArtifactsListTestsData().find(
({ tabId }) => tabId === 'eventFilters'
) as ArtifactsFixtureType;

createArtifactList(eventFiltersMock.createRequestBody.list_id);
createPerPolicyArtifact(eventFiltersMock.artifactName, eventFiltersMock.createRequestBody);

loadPage(APP_EVENT_FILTERS_PATH);

cy.getByTestSubj('EventFiltersListPage-card-header-actions-button').click();
cy.getByTestSubj('EventFiltersListPage-card-cardEditAction').click();
cy.getByTestSubj('EventFiltersListPage-flyout').should('exist');
});

it('should be able to modify after deleting value with {backspace}', () => {
cy.getByTestSubj(CONDITION_VALUE).type(' {backspace}.lnk{enter}');
cy.getByTestSubj(SUBMIT_BUTTON).click();

cy.getByTestSubj('EventFiltersListPage-flyout').should('not.exist');
cy.contains('notepad.exe.lnk');
});

it('should be able to modify without using {backspace}', () => {
cy.getByTestSubj(CONDITION_VALUE).type('.lnk{enter}');
cy.getByTestSubj(SUBMIT_BUTTON).click();

cy.getByTestSubj('EventFiltersListPage-flyout').should('not.exist');
cy.contains('notepad.exe.lnk');
});

it('should show suggestions when filter value is cleared', () => {
cy.getByTestSubj(CONDITION_VALUE).clear();
cy.getByTestSubj(CONDITION_VALUE).type('aaaaaaaaaaaaaa as custom input');
cy.get('button[role="option"]').should('have.length', 0);

cy.getByTestSubj(CONDITION_VALUE).find('input').clear();
cy.get('button[role="option"]').should('have.length.above', 1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface FormEditingDescription {
}>;
}

interface ArtifactsFixtureType {
export interface ArtifactsFixtureType {
title: string;
pagePrefix: string;
tabId: string;
Expand Down Expand Up @@ -275,10 +275,10 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [
list_id: ENDPOINT_ARTIFACT_LISTS.eventFilters.id,
entries: [
{
field: 'agent.id',
field: 'process.name',
operator: 'included',
type: 'match',
value: 'mr agent',
value: 'notepad.exe',
},
],
os_types: ['windows'],
Expand Down