From 03327a8c2140ab16c74ee7d3bb3da39cfb670c7b Mon Sep 17 00:00:00 2001
From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com>
Date: Mon, 2 Dec 2024 16:37:33 +0100
Subject: [PATCH] Support for CDB lists beginning with quotes (#7171)
* CDB list (list-editor.tsx): Change split to match
* Changelog: Entry added
* Add validation to cdb list inputs
* Test(list-editor): Add some test
* Validation(list-editor.tsx): start and end with quotes
* Test(list-editor): Add case with ipv6
* Request change(list-editor): Applying requested changes
* Test(error cases): Add test to error cases to key or value validation
* Validation(list-editor): Add colon validation.
* Validation (list-editor): Apply comments
* Validation(list-editor): Fix error found on test cration
* Test jest(list-editor): Add test to add and remove
* Test jest(list-editor): Add test to save CDB list
* Test jest(list-editor): Add test to edit and cancel edit
* Validations(list-editor): Refactor validations functions
* Validation(list-editor): Fix warning in console
* Test jest:(list-editor): Add new cases
* Test jest:(list-editor): Add new tests
* Test jest(list-editor): Add test name and path exist
---
CHANGELOG.md | 2 +-
.../cdblists/views/list-editor.test.tsx | 413 ++++++++++++++++++
.../management/cdblists/views/list-editor.tsx | 232 ++++++++--
3 files changed, 614 insertions(+), 33 deletions(-)
create mode 100644 plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12cf9a42f8..63e802cb03 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ All notable changes to the Wazuh app project will be documented in this file.
### Fixed
-- Added ending quotes to CDB Lists keys [#7159](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7159)
+- Fixed rendering of rows in CDB list table when it starts with quotes. [#7171](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7171)
## Wazuh v4.10.1 - OpenSearch Dashboards 2.16.0 - Revision 00
diff --git a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx
new file mode 100644
index 0000000000..ceb9fe77e5
--- /dev/null
+++ b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx
@@ -0,0 +1,413 @@
+import React from 'react';
+import '@testing-library/jest-dom';
+import { render, fireEvent, screen, waitFor } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import configureMockStore from 'redux-mock-store';
+import WzListEditor from './list-editor';
+
+jest.mock('../../../../../../react-services/common-services', () => ({
+ getErrorOrchestrator: () => ({
+ handleError: () => {},
+ }),
+}));
+
+jest.mock('../../common/resources-handler', () => ({
+ ResourcesHandler: jest.fn().mockImplementation(() => ({
+ updateFile: jest.fn().mockResolvedValue({
+ data: {
+ error: 0,
+ data: 'Success',
+ },
+ }),
+ })),
+ ResourcesConstants: {
+ LISTS: 'lists',
+ },
+ resourceDictionary: {
+ lists: {
+ resourcePath: '/lists',
+ permissionResource: value => `list:file:${value}`,
+ },
+ },
+}));
+
+const mockAdd = jest.fn();
+jest.mock('../../../../../../kibana-services', () => ({
+ getToasts: () => ({
+ add: mockAdd,
+ }),
+}));
+
+const mockStore = configureMockStore();
+const store = mockStore({
+ appStateReducers: {
+ userAccount: {
+ administrator: true,
+ },
+ withUserLogged: true,
+ userPermissions: {
+ 'lists:read': { '*:*:*': 'allow' },
+ },
+ },
+});
+
+describe('WzListEditor', () => {
+ describe('Without data in the list', () => {
+ it('should handle empty list content correctly', async () => {
+ const emptyListContent = {
+ content: '',
+ name: false,
+ path: '/lists/startTest',
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText('No results...')).toBeInTheDocument();
+
+ const saveButton = screen.getByText('Save');
+
+ expect(saveButton.closest('button')).toBeDisabled();
+
+ const addNewEntryButton = screen.getByText('Add new entry');
+
+ fireEvent.click(addNewEntryButton);
+
+ const keyInput = screen.getByPlaceholderText('Key');
+ const valueInput = screen.getByPlaceholderText('Value');
+
+ fireEvent.change(keyInput, { target: { value: 'testAddKey' } });
+ fireEvent.change(valueInput, { target: { value: 'testAddValue' } });
+
+ const addButton = screen.getByText('Add');
+
+ expect(addButton.closest('button')).not.toBeDisabled();
+
+ fireEvent.click(addButton);
+
+ expect(screen.getByText('testAddKey')).toBeInTheDocument();
+ expect(screen.getByText('testAddValue')).toBeInTheDocument();
+
+ expect(saveButton.closest('button')).not.toBeDisabled();
+
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockAdd).toHaveBeenCalledWith({
+ title: 'Invalid name',
+ color: 'warning',
+ text: 'CDB list name cannot be empty',
+ toastLifeTimeMs: 3000,
+ });
+ });
+
+ const nameInput = screen.getByPlaceholderText('New CDB list name');
+
+ fireEvent.change(nameInput, { target: { value: 'test' } });
+
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockAdd).toHaveBeenCalledWith({
+ title: 'Success',
+ color: 'success',
+ text: 'CBD List successfully created',
+ toastLifeTimeMs: 3000,
+ });
+ });
+ });
+ });
+
+ describe('With data in the list', () => {
+ const cdblist = [
+ {
+ key: 'test',
+ value: 'testValue',
+ },
+ {
+ key: '":test"',
+ value: '":testValue"',
+ },
+ {
+ key: 'test1',
+ value: '":key"',
+ },
+ {
+ key: '":test1"',
+ value: 'value1',
+ },
+ {
+ key: '"test2"',
+ value: '"value2"',
+ },
+ {
+ key: 'test3',
+ value: '',
+ },
+ {
+ key: '"a0:a0:a0:a0:a0:a0"',
+ value: '',
+ },
+ ];
+
+ const messagesError = {
+ quotesError: 'Must start and end with quotes or have no quotes at all',
+ colonError: 'Must start and end with quotes when using colon',
+ simbolsError:
+ 'Must not contain simbols when using quotes(only letters, numbers and colon)',
+ };
+
+ beforeEach(() => {
+ const cdblistMap = cdblist.map(item => {
+ return `${item.key}:${item.value}`;
+ });
+
+ const listContent = {
+ name: 'testName',
+ content: `${cdblistMap.join('\n')}`,
+ path: '/lists/test',
+ };
+
+ render(
+
+
+ ,
+ );
+ });
+
+ afterEach(() => {
+ mockAdd.mockClear();
+ });
+
+ it('should render the component', () => {
+ expect(screen.getByText('testName')).toBeInTheDocument();
+ expect(screen.getByText('/lists/test')).toBeInTheDocument();
+
+ cdblist.forEach(item => {
+ expect(screen.getByText(item.key)).toBeInTheDocument();
+ if (!(item.value === '')) {
+ expect(screen.getByText(item.value)).toBeInTheDocument();
+ }
+ expect(screen.queryByText(`${item.key}:${item.value}`)).toBeFalsy();
+ });
+ });
+
+ it('should delete the item correctly', async () => {
+ const deleteButton = screen.queryAllByTestId('deleteButton');
+ fireEvent.click(deleteButton[0].closest('button'));
+
+ expect(screen.queryByText(cdblist[0].key)).toBeFalsy();
+ });
+
+ it('should edit the value correctly or cancel the edit', async () => {
+ expect(screen.queryByText('newValue')).toBeFalsy();
+
+ const editButton = screen.queryAllByTestId('editButton');
+
+ fireEvent.click(editButton[0]);
+
+ const valueInput = screen.getByPlaceholderText('New value');
+
+ fireEvent.change(valueInput, { target: { value: 'newValue' } });
+
+ const saveEditButton = screen.getByTestId('saveEditButton');
+
+ fireEvent.click(saveEditButton.closest('button'));
+
+ expect(screen.getByText('newValue')).toBeInTheDocument();
+
+ fireEvent.click(editButton[1]);
+
+ fireEvent.change(valueInput, { target: { value: 'newValue2' } });
+
+ const cancelButton = screen.getByTestId('cancelEditButton');
+
+ fireEvent.click(cancelButton.closest('button'));
+
+ expect(screen.queryByText('newValue2')).toBeFalsy();
+ });
+
+ it('should update file correctly', async () => {
+ const button = screen.getByText('Add new entry');
+
+ fireEvent.click(button);
+
+ const keyInput = screen.getByPlaceholderText('Key');
+ const valueInput = screen.getByPlaceholderText('Value');
+
+ fireEvent.change(keyInput, { target: { value: 'newKey' } });
+ fireEvent.change(valueInput, { target: { value: 'newValue' } });
+
+ const addButton = screen.getByText('Add');
+
+ expect(addButton.closest('button')).not.toBeDisabled();
+
+ fireEvent.click(addButton);
+
+ const saveButton = screen.getByText('Save');
+
+ expect(saveButton.closest('button')).not.toBeDisabled();
+
+ fireEvent.click(saveButton);
+
+ expect(screen.getByText('newKey')).toBeInTheDocument();
+ expect(screen.getByText('newValue')).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(mockAdd).toHaveBeenCalledWith({
+ title: 'Success',
+ color: 'success',
+ text: 'CBD List updated',
+ toastLifeTimeMs: 3000,
+ });
+ });
+ });
+
+ it('should render the message when try to add duplicated key', async () => {
+ expect(screen.queryAllByText('test')).toHaveLength(1);
+
+ const button = screen.getByText('Add new entry');
+ fireEvent.click(button);
+
+ const keyInput = screen.getByPlaceholderText('Key');
+ const valueInput = screen.getByPlaceholderText('Value');
+
+ fireEvent.change(keyInput, { target: { value: 'test' } });
+ fireEvent.change(valueInput, { target: { value: 'testValue' } });
+
+ const addButton = screen.getByText('Add');
+ fireEvent.click(addButton);
+
+ expect(mockAdd).toHaveBeenCalledWith({
+ title: 'Error',
+ color: 'danger',
+ text: (
+
+ test key already exists
+
+ ),
+ toastLifeTimeMs: 3000,
+ });
+
+ expect(screen.queryAllByText('test')).toHaveLength(1);
+ });
+
+ it.each`
+ key | value
+ ${'key'} | ${'value'}
+ ${'"key"'} | ${'value'}
+ ${'"key"'} | ${'"value"'}
+ ${'"key:key"'} | ${'value'}
+ ${'"key:key"'} | ${'"value:value"'}
+ ${'"key"'} | ${'"value:value"'}
+ ${'key'} | ${'"value:value"'}
+ ${'key'} | ${''}
+ ${'"key"'} | ${''}
+ ${'"key:key"'} | ${''}
+ ${'"b1:b1:b1:b1:b1:b1"'} | ${'test6'}
+ `(
+ 'should add keys($key) and values($value) correctly',
+ async ({ key, value }: { key: string; value: string }) => {
+ const button = screen.getByText('Add new entry');
+ fireEvent.click(button);
+
+ const keyInput = screen.getByPlaceholderText('Key');
+ const valueInput = screen.getByPlaceholderText('Value');
+
+ fireEvent.change(keyInput, { target: { value: key } });
+ fireEvent.change(valueInput, { target: { value: value } });
+
+ const addButton = screen.getByText('Add');
+
+ expect(addButton.closest('button')).not.toBeDisabled();
+
+ fireEvent.click(addButton);
+
+ expect(screen.getByText(key)).toBeInTheDocument();
+ if (!(value === '')) {
+ expect(screen.getByText(value)).toBeInTheDocument();
+ }
+
+ const saveButton = screen.getByText('Save');
+
+ expect(saveButton.closest('button')).not.toBeDisabled();
+
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockAdd).toHaveBeenCalledWith({
+ title: 'Success',
+ color: 'success',
+ text: 'CBD List updated',
+ toastLifeTimeMs: 3000,
+ });
+ });
+ },
+ );
+
+ it.each`
+ key | value | quotesError | colonError | simbolsError
+ ${'":key'} | ${'value'} | ${messagesError.quotesError} | ${messagesError.colonError} | ${''}
+ ${'".key"'} | ${'":value'} | ${messagesError.quotesError} | ${messagesError.colonError} | ${messagesError.simbolsError}
+ ${'key"key'} | ${'"value(*&"'} | ${messagesError.quotesError} | ${''} | ${messagesError.simbolsError}
+ ${'key'} | ${'value"value'} | ${messagesError.quotesError} | ${''} | ${''}
+ ${'"key!@#"'} | ${'value"'} | ${messagesError.quotesError} | ${''} | ${messagesError.simbolsError}
+ ${'"key."'} | ${'"value'} | ${messagesError.quotesError} | ${''} | ${messagesError.simbolsError}
+ ${'"key"key"'} | ${'value'} | ${messagesError.quotesError} | ${''} | ${messagesError.simbolsError}
+ ${'key'} | ${'"value"value"'} | ${messagesError.quotesError} | ${''} | ${messagesError.simbolsError}
+ ${'key:key'} | ${'"value;"'} | ${''} | ${messagesError.colonError} | ${messagesError.simbolsError}
+ ${'key:"key"'} | ${'value'} | ${messagesError.quotesError} | ${messagesError.colonError} | ${''}
+ ${'key'} | ${':value'} | ${''} | ${messagesError.colonError} | ${''}
+ ${'"key:key"'} | ${'"value":'} | ${messagesError.quotesError} | ${messagesError.colonError} | ${''}
+ `(
+ 'should render the message when try to add invalid key($key) or value($value)',
+ ({
+ key,
+ value,
+ quotesError,
+ colonError,
+ simbolsError,
+ }: {
+ key: string;
+ value: string;
+ quotesError: string;
+ colonError: string;
+ simbolsError: string;
+ }) => {
+ const button = screen.getByText('Add new entry');
+ fireEvent.click(button);
+
+ const keyInput = screen.getByPlaceholderText('Key');
+ const valueInput = screen.getByPlaceholderText('Value');
+
+ fireEvent.change(keyInput, { target: { value: key } });
+ fireEvent.change(valueInput, { target: { value: value } });
+
+ const addButton = screen.getByText('Add');
+
+ expect(addButton.closest('button')).toBeDisabled();
+
+ if (quotesError) {
+ expect(screen.getByText(quotesError)).toBeInTheDocument();
+ } else {
+ expect(screen.queryByText(messagesError.quotesError)).toBeFalsy();
+ }
+
+ if (colonError) {
+ expect(screen.getByText(colonError)).toBeInTheDocument();
+ } else {
+ expect(screen.queryByText(messagesError.colonError)).toBeFalsy();
+ }
+
+ if (simbolsError) {
+ expect(screen.getByText(simbolsError)).toBeInTheDocument();
+ } else {
+ expect(screen.queryByText(messagesError.simbolsError)).toBeFalsy();
+ }
+ },
+ );
+ });
+});
diff --git a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx
index 5900b37483..924773119b 100644
--- a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx
+++ b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx
@@ -23,6 +23,7 @@ import {
EuiFieldText,
EuiSpacer,
EuiPanel,
+ EuiFormRow,
} from '@elastic/eui';
import { connect } from 'react-redux';
@@ -45,7 +46,22 @@ import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orch
import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants';
import { getErrorOrchestrator } from '../../../../../../react-services/common-services';
+type FieldTypes = 'key' | 'value' | 'edit';
+type FieldStateTypes = 'addingKey' | 'addingValue' | 'editingValue';
+type CheckingsTypes = 'quotes' | 'colon' | 'simbols';
+interface ValidationInterface {
+ condition: boolean;
+ checking: CheckingsTypes;
+}
+
class WzListEditor extends Component {
+ private readonly messagesError = {
+ quotes: 'Must start and end with quotes or have no quotes at all',
+ colon: 'Must start and end with quotes when using colon',
+ simbols:
+ 'Must not contain simbols when using quotes(only letters, numbers and colon)',
+ };
+
constructor(props) {
super(props);
this.state = {
@@ -58,6 +74,7 @@ class WzListEditor extends Component {
editingValue: '',
newListName: '',
showWarningRestart: false,
+ isInvalid: [],
};
this.items = {};
@@ -91,16 +108,29 @@ class WzListEditor extends Component {
* Save in the state as object the items for an easy modification by key-value
* @param {String} content
*/
- contentToObject(content) {
- const items = {};
+ contentToObject(content: string) {
+ const items: {
+ [key: string]: string;
+ } = {};
const lines = content.split('\n');
+
+ const regex = /^((?:[^:"]*|"[^"]*")*):(.*)$/;
+
lines.forEach(line => {
- const split = line.startsWith('"') ? line.split('":') : line.split(':');
- // All keys with multiple colons (:) should end with a quotation mark (")
- const key = split[0].startsWith('"') ? split[0] + '"' : split[0];
- const value = split[1] || '';
- if (key) items[key] = value; // Prevent add empty keys
+ // Regex splitting the first : and ignoring the ones inside quotes
+ const match = line.match(regex);
+
+ if (match) {
+ const [, key, value] = match;
+ const trimmedKey = key.trim();
+ const trimmedValue = value.trim();
+
+ if (trimmedKey) {
+ items[trimmedKey] = trimmedValue;
+ }
+ }
});
+
return items;
}
@@ -149,7 +179,6 @@ class WzListEditor extends Component {
await this.resourcesHandler.updateFile(name, raw, overwrite);
if (!addingNew) {
const file = { name: name, content: raw, path: path };
- this.props.updateListContent(file);
this.setState({ showWarningRestart: true });
this.showToast(
'success',
@@ -157,6 +186,7 @@ class WzListEditor extends Component {
'CBD List successfully created',
3000,
);
+ this.props.updateListContent(file);
} else {
this.setState({ showWarningRestart: true });
this.showToast('success', 'Success', 'CBD List updated', 3000);
@@ -178,7 +208,7 @@ class WzListEditor extends Component {
this.setState({ isSaving: false });
}
- showToast = (color, title, text, time) => {
+ showToast = (color: string, title: string, text: string, time: number) => {
getToasts().add({
color: color,
title: title,
@@ -200,18 +230,24 @@ class WzListEditor extends Component {
};
onChangeKey = e => {
+ this.executeValidation(e.target.value, 'key');
+
this.setState({
addingKey: e.target.value,
});
};
onChangeValue = e => {
+ this.executeValidation(e.target.value, 'value');
+
this.setState({
addingValue: e.target.value,
});
};
onChangeEditingValue = e => {
+ this.executeValidation(e.target.value, 'edit');
+
this.setState({
editingValue: e.target.value,
});
@@ -243,11 +279,101 @@ class WzListEditor extends Component {
];
};
+ private executeValidation = (value: string, field: FieldTypes) => {
+ const startsWithQuote = value.startsWith('"');
+ const endsWithQuote = value.endsWith('"');
+ const valueWithoutQuotes = value.slice(1, -1);
+ // For the simbols validation
+ const hasNotSimbols = valueWithoutQuotes.match(/^[a-zA-Z0-9:]+$/);
+ // For the colon validation outside quotes
+ const hasColon = value.includes(':');
+ // If the value has a colon it starts and ends with quotes
+ const hasMiddleQuotes = valueWithoutQuotes.includes('"');
+ const valueLength = value.length !== 1;
+
+ const quotesCondition =
+ !hasMiddleQuotes &&
+ ((startsWithQuote && endsWithQuote && valueLength) ||
+ (!startsWithQuote && !endsWithQuote));
+
+ const colonCondition = !(startsWithQuote && endsWithQuote) && hasColon;
+
+ const simbolsCondition =
+ !hasNotSimbols && startsWithQuote && endsWithQuote && valueLength;
+
+ const validationArray: ValidationInterface[] = [
+ {
+ checking: 'quotes',
+ condition: !quotesCondition,
+ },
+ {
+ checking: 'colon',
+ condition: colonCondition,
+ },
+ {
+ checking: 'simbols',
+ condition: simbolsCondition,
+ },
+ ];
+
+ validationArray.forEach(({ checking, condition }) => {
+ this.genericValidation({
+ field,
+ condition,
+ cheking: checking,
+ });
+ });
+ };
+
+ genericValidation = ({
+ field,
+ condition,
+ cheking,
+ }: {
+ field: FieldTypes;
+ condition: boolean;
+ cheking: CheckingsTypes;
+ }) => {
+ if (condition) {
+ const existsError = this.state.isInvalid.some(
+ error =>
+ error.field === field &&
+ error.message === this.messagesError[cheking],
+ );
+
+ // Avoid adding the same error multiple times
+ if (existsError) {
+ return;
+ }
+
+ this.setState(prevState => ({
+ isInvalid: [
+ ...prevState.isInvalid,
+ {
+ field,
+ message: this.messagesError[cheking],
+ },
+ ],
+ }));
+ return;
+ }
+ this.setState(prevState => ({
+ isInvalid: prevState.isInvalid.filter(
+ error =>
+ !(
+ error.field === field &&
+ error.message === this.messagesError[cheking]
+ ),
+ ),
+ }));
+ };
+
/**
* Append a key value to this.items and after that if everything works ok re-create the array for the table
*/
addItem() {
const { addingKey, addingValue } = this.state;
+ const hasinvalidState = this.state.isInvalid.length > 0;
if (!addingKey || Object.keys(this.items).includes(addingKey)) {
this.showToast(
'danger',
@@ -259,6 +385,15 @@ class WzListEditor extends Component {
);
return;
}
+ if (hasinvalidState) {
+ this.showToast(
+ 'danger',
+ 'Error',
+ `Key and value ${this.messages.quotesError.toLowerCase()}`,
+ 3000,
+ );
+ return;
+ }
this.items[addingKey] = addingValue;
const itemsArr = this.contentToArray(this.items);
this.setState({
@@ -368,9 +503,42 @@ class WzListEditor extends Component {
);
}
+ inputValidation({
+ field,
+ value,
+ onChange,
+ placeholder,
+ }: {
+ field: FieldTypes;
+ value: FieldStateTypes;
+ onChange: (e: any) => void;
+ placeholder: string;
+ }) {
+ const isInvalid = this.state.isInvalid.some(error => error.field === field);
+ const errorMessages: string[] = [];
+ this.state.isInvalid.forEach(error => {
+ error.field === field && errorMessages.push(error.message);
+ });
+
+ return (
+
+
+
+ );
+ }
+
renderAdd() {
const { addingKey, addingValue } = this.state;
+ const hasInvalidState = this.state.isInvalid.length > 0;
+
return (
{this.state.isPopoverOpen && (
@@ -378,23 +546,21 @@ class WzListEditor extends Component {
-
+ {this.inputValidation({
+ field: 'key',
+ value: addingKey,
+ onChange: this.onChangeKey,
+ placeholder: 'Key',
+ })}
-
+ {this.inputValidation({
+ field: 'value',
+ value: addingValue,
+ onChange: this.onChangeValue,
+ placeholder: 'Value',
+ })}
@@ -402,8 +568,8 @@ class WzListEditor extends Component {
this.addItem()}
+ disabled={hasInvalidState}
>
Add
@@ -470,14 +636,12 @@ class WzListEditor extends Component {
sortable: true,
render: (value, item) => {
if (this.state.editing === item.key) {
- return (
-
- );
+ return this.inputValidation({
+ field: 'edit',
+ value: this.state.editingValue,
+ onChange: this.onChangeEditingValue,
+ placeholder: 'New value',
+ });
} else {
return {value};
}
@@ -498,6 +662,7 @@ class WzListEditor extends Component {
this.setEditedValue();
}}
color='primary'
+ data-testid='saveEditButton'
/>
@@ -506,6 +671,7 @@ class WzListEditor extends Component {
iconType='cross'
onClick={() => this.setState({ editing: false })}
color='danger'
+ data-testid='cancelEditButton'
/>
@@ -526,6 +692,7 @@ class WzListEditor extends Component {
});
}}
color='primary'
+ data-testid='editButton'
/>
this.deleteItem(item.key)}
color='danger'
+ data-testid='deleteButton'
/>
);