diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx index 03fb17d0fe906..3b373fa7f3e15 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx @@ -7,11 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useEffect, useContext, memo } from 'react'; +import React, { useEffect, useContext, memo, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; -import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { EuiDataGridCellValueElementProps, EuiTreeViewProps } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTreeView, +} from '@elastic/eui'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { getDataViewFieldOrCreateFromColumnMeta } from '@kbn/data-view-utils'; import type { @@ -22,12 +28,517 @@ import type { import { formatFieldValueReact } from '@kbn/discover-utils'; import { UnifiedDataTableContext } from '../table_context'; import type { CustomCellRenderer } from '../types'; -import { SourceDocument } from '../components/source_document'; import SourcePopoverContent from '../components/source_popover_content'; import { DataTablePopoverCellValue } from '../components/data_table_cell_value'; +import { getInnerColumns } from './columns'; export const CELL_CLASS = 'unifiedDataTable__cellValue'; +type CellTreePreviewValue = string | number | boolean | null; +type CellTreeJsonValue = + | Record + | unknown[] + | string + | number + | boolean + | null + | undefined; + +const HEAVY_NESTED_OBJECT_PREVIEW: CellTreeJsonValue = Object.fromEntries( + Array.from({ length: 200 }, (_, index) => { + const fieldNumber = index + 1; + + return [ + `object_${fieldNumber}`, + { + id: `id-${fieldNumber}`, + enabled: fieldNumber % 2 === 0, + count: fieldNumber, + tags: ['metrics', `object_${fieldNumber}`, 'demo'], + agent: { + name: 'policy_template', + version: '0.1', + ephemeral_id: `agent-${fieldNumber}`, + }, + attributes: { + event: { + domain: 'events:metrics', + name: 'metrics and events', + kind: 'metric', + }, + k8s: { + namespace: 'default', + pod: { + name: `metrics-pod-${fieldNumber}`, + uid: `pod-${fieldNumber}`, + labels: { + app: 'metrics', + tier: 'backend', + }, + }, + }, + }, + histogram: { + values: [0.1, 0.2, 0.3, 0.4, 0.5], + counts: [3, 7, 23, 12, 6], + }, + }, + ]; + }) +); +const ENABLE_CELL_TREE_HEAVY_DEMO = true; + +const renderCellTreeValue = (value: CellTreePreviewValue) => ( + ({ + color: + value === null + ? euiTheme.colors.textSubdued + : typeof value === 'string' + ? euiTheme.colors.dangerText + : euiTheme.colors.successText, + whiteSpace: 'nowrap', + })} + > + {value === null ? 'null' : typeof value === 'string' ? `"${value}"` : String(value)} + +); + +const renderCellTreeLabel = ({ + name, + value, + count, + collectionType = 'object', + showFilterActions = false, +}: { + name: string; + value?: CellTreePreviewValue; + count?: number; + collectionType?: 'array' | 'object'; + showFilterActions?: boolean; +}) => ( + ({ + alignItems: 'center', + display: 'inline-flex', + gap: euiTheme.size.xxs, + fontFamily: euiTheme.font.familyCode, + fontSize: euiTheme.size.m, + lineHeight: euiTheme.size.m, + minWidth: 0, + + '&:hover .unifiedDataTable__cellTreeFilterActions, &:focus-within .unifiedDataTable__cellTreeFilterActions': + { + opacity: 1, + }, + })} + > + + ({ color: euiTheme.colors.textSubdued })}>{'"'} + ({ color: euiTheme.colors.text })}>{name} + ({ color: euiTheme.colors.textSubdued })}>{'"'} + + ({ color: euiTheme.colors.textSubdued })}>: + {typeof count === 'number' ? ( + ({ color: euiTheme.colors.text })}> + {collectionType === 'array' ? '[' : '{'} + + ) : null} + {typeof value !== 'undefined' ? ( + <> + {renderCellTreeValue(value)} + ({ color: euiTheme.colors.textSubdued })}>, + + ) : null} + {showFilterActions ? ( + ({ + alignItems: 'center', + display: 'inline-flex', + gap: euiTheme.size.xs, + opacity: 0, + transition: `opacity ${euiTheme.animation.fast} ${euiTheme.animation.resistance}`, + })} + > + ) => { + event.preventDefault(); + event.stopPropagation(); + }} + size="xs" + /> + ) => { + event.preventDefault(); + event.stopPropagation(); + }} + size="xs" + /> + + ) : null} + +); + +const CELL_TREE_VIEW_PREVIEW_MIN_VISIBLE_ITEMS = 5; +const CELL_TREE_VIEW_PREVIEW_INITIAL_VISIBLE_ITEMS = 10; +const CELL_TREE_VIEW_PREVIEW_INCREMENT = 10; + +type CellTreeViewPreviewItem = EuiTreeViewProps['items'][number]; +type CellTreeViewExpansionState = Record; + +const isCellTreeObject = (value: CellTreeJsonValue): value is Record => { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +}; + +const getCellTreeItemId = (path: string[]) => { + return `cell-tree-${path.join('__')}`; +}; + +const normalizeCellTreePrimitiveValue = (value: CellTreeJsonValue): CellTreePreviewValue => { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + return null; +}; + +const buildCellTreeItem = ({ + name, + path, + value, +}: { + name: string; + path: string[]; + value: CellTreeJsonValue; +}): CellTreeViewPreviewItem => { + if (Array.isArray(value)) { + return { + id: getCellTreeItemId(path), + label: renderCellTreeLabel({ name, count: value.length, collectionType: 'array' }), + children: value.map((childValue, index) => + buildCellTreeItem({ + name: String(index), + path: [...path, String(index)], + value: childValue as CellTreeJsonValue, + }) + ), + }; + } + + if (isCellTreeObject(value)) { + const entries = Object.entries(value); + + return { + id: getCellTreeItemId(path), + label: renderCellTreeLabel({ name, count: entries.length }), + children: entries.map(([childName, childValue]) => + buildCellTreeItem({ + name: childName, + path: [...path, childName], + value: childValue as CellTreeJsonValue, + }) + ), + }; + } + + return { + id: getCellTreeItemId(path), + label: renderCellTreeLabel({ + name, + showFilterActions: true, + value: normalizeCellTreePrimitiveValue(value), + }), + }; +}; + +const buildCellTreeItems = (json: CellTreeJsonValue): EuiTreeViewProps['items'] => { + if (Array.isArray(json)) { + return json.map((value, index) => + buildCellTreeItem({ + name: String(index), + path: [String(index)], + value: value as CellTreeJsonValue, + }) + ); + } + + if (isCellTreeObject(json)) { + return Object.entries(json).map(([name, value]) => + buildCellTreeItem({ + name, + path: [name], + value: value as CellTreeJsonValue, + }) + ); + } + + return [ + buildCellTreeItem({ + name: 'value', + path: ['value'], + value: json, + }), + ]; +}; + +const getCellTreeSourceJson = ({ + row, + dataView, + columnId, + useTopLevelObjectColumns, +}: { + row: DataTableRecord; + dataView: DataView; + columnId: string; + useTopLevelObjectColumns: boolean; +}): CellTreeJsonValue => { + const sourceJson = useTopLevelObjectColumns + ? getInnerColumns(row.raw.fields as Record, columnId) + : row.raw; + const sourceTreeJson = sourceJson as CellTreeJsonValue; + const { timeFieldName } = dataView; + + if (!timeFieldName || typeof row.flattened[timeFieldName] === 'undefined') { + return sourceTreeJson; + } + + if (isCellTreeObject(sourceTreeJson) && typeof sourceTreeJson[timeFieldName] === 'undefined') { + return { + [timeFieldName]: row.flattened[timeFieldName], + ...sourceTreeJson, + }; + } + + return sourceTreeJson; +}; + +const getExpandableCellTreeItemIds = (items: EuiTreeViewProps['items']): string[] => + items.flatMap((item) => { + if (!item.children) { + return []; + } + + return [item.id, ...getExpandableCellTreeItemIds(item.children)]; + }); + +const setCellTreeItemsExpansionState = ( + items: EuiTreeViewProps['items'], + expansionState: CellTreeViewExpansionState +): EuiTreeViewProps['items'] => + items.map((item): CellTreeViewPreviewItem => { + const children = item.children + ? setCellTreeItemsExpansionState(item.children, expansionState) + : undefined; + + return { + ...item, + ...(children ? { children, isExpanded: expansionState[item.id] ?? false } : {}), + }; + }); + +const CellTreeViewPreview = ({ json }: { json: CellTreeJsonValue }) => { + const cellTreeItems = useMemo(() => buildCellTreeItems(json), [json]); + const [visibleItemsCount, setVisibleItemsCount] = useState( + CELL_TREE_VIEW_PREVIEW_INITIAL_VISIBLE_ITEMS + ); + const [expansionState, setExpansionState] = useState({}); + const [treeRenderVersion, setTreeRenderVersion] = useState(0); + const visibleItemsLimit = Math.min(visibleItemsCount, cellTreeItems.length); + const hiddenItemCount = Math.max(cellTreeItems.length - visibleItemsLimit, 0); + const canHideRows = + visibleItemsLimit > Math.min(CELL_TREE_VIEW_PREVIEW_MIN_VISIBLE_ITEMS, cellTreeItems.length); + const rawVisibleItems = cellTreeItems.slice(0, visibleItemsLimit); + const visibleItems = setCellTreeItemsExpansionState(rawVisibleItems, expansionState); + + return ( + <> + + + ) => { + event.preventDefault(); + event.stopPropagation(); + const visibleExpandableItemIds = getExpandableCellTreeItemIds(rawVisibleItems); + setExpansionState((previousExpansionState) => ({ + ...previousExpansionState, + ...Object.fromEntries(visibleExpandableItemIds.map((id) => [id, true])), + })); + setTreeRenderVersion((previousTreeRenderVersion) => previousTreeRenderVersion + 1); + }} + size="xs" + > + {i18n.translate('unifiedDataTable.grid.cellTreeViewPreview.expandVisibleButtonLabel', { + defaultMessage: 'Expand visible', + })} + + + + ) => { + event.preventDefault(); + event.stopPropagation(); + const visibleExpandableItemIds = getExpandableCellTreeItemIds(rawVisibleItems); + setExpansionState((previousExpansionState) => ({ + ...previousExpansionState, + ...Object.fromEntries(visibleExpandableItemIds.map((id) => [id, false])), + })); + setTreeRenderVersion((previousTreeRenderVersion) => previousTreeRenderVersion + 1); + }} + size="xs" + > + {i18n.translate( + 'unifiedDataTable.grid.cellTreeViewPreview.collapseVisibleButtonLabel', + { + defaultMessage: 'Collapse visible', + } + )} + + + +
({ + fontFamily: euiTheme.font.familyCode, + fontSize: euiTheme.size.m, + lineHeight: euiTheme.size.m, + + '.euiTreeView__node': { + marginBottom: 0, + }, + })} + > +
{'{'}
+ +
{'}'}
+
+ {canHideRows || hiddenItemCount > 0 ? ( + + {canHideRows ? ( + + ) => { + event.preventDefault(); + event.stopPropagation(); + const minVisibleItems = cellTreeItems.slice( + 0, + CELL_TREE_VIEW_PREVIEW_MIN_VISIBLE_ITEMS + ); + const minVisibleExpandableItemIds = getExpandableCellTreeItemIds(minVisibleItems); + + setVisibleItemsCount( + Math.min(CELL_TREE_VIEW_PREVIEW_MIN_VISIBLE_ITEMS, cellTreeItems.length) + ); + setExpansionState((previousExpansionState) => ({ + ...previousExpansionState, + ...Object.fromEntries(minVisibleExpandableItemIds.map((id) => [id, false])), + })); + setTreeRenderVersion( + (previousTreeRenderVersion) => previousTreeRenderVersion + 1 + ); + }} + size="xs" + > + {i18n.translate( + 'unifiedDataTable.grid.cellTreeViewPreview.hideAllRowsButtonLabel', + { + defaultMessage: 'Hide all rows', + } + )} + + + ) : null} + {hiddenItemCount > 0 ? ( + <> + + ) => { + event.preventDefault(); + event.stopPropagation(); + setVisibleItemsCount((previousVisibleItemsCount) => + Math.min( + previousVisibleItemsCount + CELL_TREE_VIEW_PREVIEW_INCREMENT, + cellTreeItems.length + ) + ); + }} + size="xs" + > + {i18n.translate( + 'unifiedDataTable.grid.cellTreeViewPreview.showMoreFieldsButtonLabel', + { + defaultMessage: 'Show 10 more fields', + } + )} + + + + ) => { + event.preventDefault(); + event.stopPropagation(); + setVisibleItemsCount(cellTreeItems.length); + }} + size="xs" + > + {i18n.translate( + 'unifiedDataTable.grid.cellTreeViewPreview.showFullObjectButtonLabel', + { + defaultMessage: 'Show full object', + } + )} + + + + ) : null} + + ) : null} + + ); +}; + const IS_JEST_ENVIRONMENT = typeof jest !== 'undefined'; export const getRenderCellValueFn = ({ @@ -140,31 +651,51 @@ export const getRenderCellValueFn = ({ (isPlainRecord && columnId === '_source') ) { return ( - +
({ + padding: `${euiTheme.size.xs} 0`, + })} + > + +
+ ); + } + + if (columnId === dataView.timeFieldName) { + return ( + + {formatFieldValueReact({ + value: row.flattened[columnId], + hit: row.raw, + fieldFormats, + dataView, + field, + })} + ); } return ( - - {formatFieldValueReact({ - value: row.flattened[columnId], - hit: row.raw, - fieldFormats, - dataView, - field, +
({ + padding: `${euiTheme.size.xs} 0`, })} - + > + +
); }; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap deleted file mode 100644 index 04098ebbe9ed7..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ /dev/null @@ -1,281 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should render normally 1`] = ` -Object { - "items": Array [ - Object { - "displayName": "Elastic", - "esTypes": Array [ - "keyword", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "Elastic", - "info": Array [ - "Rollup aggregations:", - "terms", - ], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "Elastic", - "searchable": true, - "type": "keyword", - }, - Object { - "displayName": "timestamp", - "esTypes": Array [ - "date", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "timestamp", - "info": Array [ - "Rollup aggregations:", - "date_histogram (interval: 30s, delay: 30s, UTC)", - ], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "timestamp", - "type": "date", - }, - Object { - "displayName": "conflictingField", - "esTypes": Array [ - "keyword", - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "conflictingField", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "conflictingField", - "type": "keyword, long", - }, - Object { - "displayName": "amount", - "esTypes": Array [ - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "amount", - "info": Array [ - "Rollup aggregations:", - "histogram (interval: 5)", - "avg", - "max", - "min", - "sum", - "value_count", - ], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "amount", - "type": "long", - }, - Object { - "displayName": "runtime", - "esTypes": Array [ - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": true, - "id": "runtime", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": "number", - "name": "runtime", - "runtimeField": Object { - "script": Object { - "source": "emit('Hello');", - }, - "type": "long", - }, - "type": "long", - }, - ], -} -`; - -exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` -Object { - "items": Array [ - Object { - "displayName": "Elastic", - "esTypes": Array [ - "keyword", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "Elastic", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "Elastic", - "searchable": true, - "type": "keyword", - }, - ], -} -`; - -exports[`IndexedFieldsTable should filter based on the schema filter 1`] = ` -Object { - "items": Array [ - Object { - "displayName": "runtime", - "esTypes": Array [ - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": true, - "id": "runtime", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": "number", - "name": "runtime", - "runtimeField": Object { - "script": Object { - "source": "emit('Hello');", - }, - "type": "long", - }, - "type": "long", - }, - ], -} -`; - -exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` -Object { - "items": Array [ - Object { - "displayName": "timestamp", - "esTypes": Array [ - "date", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "timestamp", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "timestamp", - "type": "date", - }, - ], -} -`; - -exports[`IndexedFieldsTable should render normally 1`] = ` -Object { - "items": Array [ - Object { - "displayName": "Elastic", - "esTypes": Array [ - "keyword", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "Elastic", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "Elastic", - "searchable": true, - "type": "keyword", - }, - Object { - "displayName": "timestamp", - "esTypes": Array [ - "date", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "timestamp", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "timestamp", - "type": "date", - }, - Object { - "displayName": "conflictingField", - "esTypes": Array [ - "keyword", - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "conflictingField", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "conflictingField", - "type": "keyword, long", - }, - Object { - "displayName": "amount", - "esTypes": Array [ - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": false, - "id": "amount", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": undefined, - "name": "amount", - "type": "long", - }, - Object { - "displayName": "runtime", - "esTypes": Array [ - "long", - ], - "excluded": false, - "format": "", - "hasRuntime": true, - "id": "runtime", - "info": Array [], - "isMapped": false, - "isUserEditable": false, - "kbnType": "number", - "name": "runtime", - "runtimeField": Object { - "script": Object { - "source": "emit('Hello');", - }, - "type": "long", - }, - "type": "long", - }, - ], -} -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap deleted file mode 100644 index 960b367fe4b6b..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ /dev/null @@ -1,364 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Table render conflict summary modal 1`] = ` - - - - - - - - -

- - message - , - } - } - /> -

- - } - rowHeader="firstName" - tableCaption="Field type conflicts across indices" - tableLayout="auto" - /> -
-
- - - - - -
-`; - -exports[`Table render name 1`] = ` - - - - customer - - -`; - -exports[`Table render name 2`] = ` - - - - customer - - - -`; - -exports[`Table should render conflicting type 1`] = ` - - text, long - - - - Conflict - - - -`; - -exports[`Table should render mixed, non-conflicting type 1`] = ` - - keyword, constant_keyword - -`; - -exports[`Table should render normal field name 1`] = ` - - - - Elastic - - -`; - -exports[`Table should render normal type 1`] = ` - - string - -`; - -exports[`Table should render normally 1`] = ` - -`; - -exports[`Table should render the boolean template (false) 1`] = ``; - -exports[`Table should render the boolean template (true) 1`] = ` - - Is searchable - -`; - -exports[`Table should render timestamp field name 1`] = ` - - - - timestamp - - - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index aa4482a85e695..4066342f3152d 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -7,248 +7,280 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; -import { shallow } from 'enzyme'; -import type { DataView } from '@kbn/data-views-plugin/public'; import type { IndexedFieldItem } from '../../types'; +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { coreMock, overlayServiceMock } from '@kbn/core/public/mocks'; +import { createStubDataView } from '@kbn/data-views-plugin/public/data_views/data_view.stub'; import { - TableWithoutPersist as Table, - renderFieldName, getConflictModalContent, + renderFieldName, showDelete, + TableWithoutPersist as Table, } from './table'; -import { coreMock, overlayServiceMock } from '@kbn/core/public/mocks'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; const coreStart = coreMock.createStart(); -const indexPattern = { - timeFieldName: 'timestamp', -} as DataView; +const indexPattern = createStubDataView({ + spec: { + timeFieldName: 'timestamp', + title: 'test-data-view', + }, +}); -const items: IndexedFieldItem[] = [ - { +const createIndexedField = ({ + name, + ...field +}: Pick & Partial): IndexedFieldItem => ({ + displayName: name, + excluded: false, + hasRuntime: false, + info: [], + isMapped: true, + isUserEditable: true, + kbnType: 'string', + name, + type: 'keyword', + ...field, +}); + +const items = [ + createIndexedField({ name: 'Elastic', - displayName: 'Elastic', searchable: true, - info: [], type: 'name', - kbnType: 'string', - excluded: false, - isMapped: true, - isUserEditable: true, - hasRuntime: false, - }, - { + }), + createIndexedField({ + kbnType: 'date', name: 'timestamp', - displayName: 'timestamp', type: 'date', - kbnType: 'date', - info: [], - excluded: false, - isMapped: true, - isUserEditable: true, - hasRuntime: false, - }, - { - name: 'conflictingField', - displayName: 'conflictingField', + }), + createIndexedField({ conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] }, - type: 'text, long', kbnType: 'conflict', - info: [], - excluded: false, - isMapped: true, - isUserEditable: true, - hasRuntime: false, - }, - { + name: 'conflictingField', + type: 'text, long', + }), + createIndexedField({ + hasRuntime: true, + isMapped: false, + kbnType: 'text', name: 'customer', - displayName: 'customer', type: 'keyword', - kbnType: 'text', - info: [], - excluded: false, - isMapped: false, - isUserEditable: true, + }), + createIndexedField({ hasRuntime: true, - }, - { - name: 'noedit', - displayName: 'noedit', - type: 'keyword', - kbnType: 'text', - info: [], - excluded: false, isMapped: false, isUserEditable: false, - hasRuntime: true, - }, + kbnType: 'text', + name: 'noedit', + type: 'keyword', + }), ]; -const baseProps = { +const mixedTypeItem = createIndexedField({ + name: 'mixedType', + type: 'keyword, constant_keyword', +}); + +const primitiveRuntimeField = createIndexedField({ + hasRuntime: true, + isMapped: false, + name: 'runtimePrimitive', + runtimeField: { + type: 'keyword', + }, +}); + +const compositeRuntimeField = createIndexedField({ + hasRuntime: true, + isMapped: false, + name: 'runtimeComposite', + runtimeField: { + type: 'composite', + }, +}); + +const compositeRuntimeDefinition = createIndexedField({ + hasRuntime: true, + isMapped: false, + name: 'runtimeCompositeDefinition', + runtimeField: { + type: 'composite', + }, + type: 'composite', +}); + +const baseProps: Pick, 'euiTablePersist'> = { euiTablePersist: { + onTableChange: jest.fn(), pageSize: 10, - onTableChange: () => {}, - sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + sorting: { sort: { direction: 'asc', field: 'name' } }, }, }; -const renderTable = ( - { editField } = { - editField: () => {}, - } -) => - shallow( +const renderTable = ({ + deleteField = jest.fn(), + editField = jest.fn(), + tableItems = items, +}: { + deleteField?: React.ComponentProps['deleteField']; + editField?: React.ComponentProps['editField']; + tableItems?: IndexedFieldItem[]; +} = {}) => + renderWithI18n( {}} + indexPattern={indexPattern} + items={tableItems} openModal={overlayServiceMock.createStartContract().openModal} startServices={coreStart} /> ); describe('Table', () => { - test('should render normally', () => { - expect(renderTable()).toMatchSnapshot(); + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); // Silent EUI warnings during tests }); - test('should render normal field name', () => { - const tableCell = shallow(renderTable().prop('columns')[0].render('Elastic', items[0])); - expect(tableCell).toMatchSnapshot(); + afterEach(() => { + jest.restoreAllMocks(); }); - test('should render timestamp field name', () => { - const tableCell = shallow(renderTable().prop('columns')[0].render('timestamp', items[1])); - expect(tableCell).toMatchSnapshot(); + it('should render normally', async () => { + renderTable(); + + expect(await screen.findByText('Elastic')).toBeVisible(); + expect(screen.getByText('timestamp')).toBeVisible(); + expect(screen.getByText('conflictingField')).toBeVisible(); + expect(screen.getByText('customer')).toBeVisible(); + expect(screen.getByText('noedit')).toBeVisible(); }); - test('should render the boolean template (true)', () => { - const tableCell = shallow(renderTable().prop('columns')[3].render(true)); - expect(tableCell).toMatchSnapshot(); + it('should render normal field name', async () => { + renderWithI18n(renderFieldName(items[0])); + + expect(await screen.findByText('String')).toBeVisible(); + expect(screen.getByTestId('field-name-Elastic')).toHaveTextContent('Elastic'); }); - test('should render the boolean template (false)', () => { - const tableCell = shallow(renderTable().prop('columns')[3].render(false, items[2])); - expect(tableCell).toMatchSnapshot(); + it('should render timestamp field name', async () => { + renderWithI18n(renderFieldName(items[1], indexPattern.timeFieldName)); + + expect(await screen.findByText('Date')).toBeVisible(); + expect(screen.getByTestId('field-name-timestamp')).toHaveTextContent('timestamp'); + expect(screen.getByText('Primary time field')).toBeVisible(); }); - test('should render normal type', () => { - const tableCell = shallow(renderTable().prop('columns')[1].render('string', {})); - expect(tableCell).toMatchSnapshot(); + it('should render the boolean template (true)', async () => { + renderTable(); + + expect(await screen.findByText('Is searchable')).toBeVisible(); }); - test('should render conflicting type', () => { - const tableCell = shallow( - renderTable() - .prop('columns')[1] - .render('text, long', { - kbnType: 'conflict', - conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] }, - }) + it('should render the boolean template (false)', async () => { + renderTable(); + + expect((await screen.findByText('conflictingField')).closest('tr')).not.toHaveTextContent( + 'Is searchable' ); - expect(tableCell).toMatchSnapshot(); }); - test('should render mixed, non-conflicting type', () => { - const tableCell = shallow( - renderTable().prop('columns')[1].render('keyword, constant_keyword', { - kbnType: 'string', - }) - ); - expect(tableCell).toMatchSnapshot(); + it('should render normal type', async () => { + renderTable(); + + expect((await screen.findByText('Elastic')).closest('tr')).toHaveTextContent('name'); + }); + + it('should render conflicting type', async () => { + renderTable(); + + const conflictingFieldRow = (await screen.findByText('conflictingField')).closest('tr'); + expect(conflictingFieldRow).toHaveTextContent('text, long'); + expect(conflictingFieldRow).toHaveTextContent('Conflict'); + }); + + it('should render mixed, non-conflicting type', async () => { + renderTable({ tableItems: [mixedTypeItem] }); + + const mixedTypeRow = (await screen.findByText('mixedType')).closest('tr'); + expect(mixedTypeRow).toHaveTextContent('keyword, constant_keyword'); + expect(mixedTypeRow).not.toHaveTextContent('Conflict'); }); - test('should allow edits', () => { + it('should allow edits', async () => { + const user = userEvent.setup(); const editField = jest.fn(); - // Click the edit button - renderTable({ editField }).prop('columns')[6].actions[0].onClick(); - expect(editField).toBeCalled(); + renderTable({ editField }); + + await user.click(screen.getAllByTestId('editFieldFormat')[0]); + + expect(editField).toHaveBeenCalled(); }); - test('should not allow edit or deletion for user with only read access', () => { - const editAvailable = renderTable().prop('columns')[6].actions[0].available(items[4]); - const deleteAvailable = renderTable().prop('columns')[6].actions[1].available(items[4]); - expect(editAvailable).toBeFalsy(); - expect(deleteAvailable).toBeFalsy(); + it('should not allow edit or deletion for user with only read access', async () => { + renderTable({ tableItems: [items[4]] }); + + const noEditRow = (await screen.findByText('noedit')).closest('tr'); + expect(noEditRow).toBeVisible(); + expect(screen.queryByTestId('editFieldFormat')).not.toBeInTheDocument(); + expect(screen.queryByTestId('deleteField')).not.toBeInTheDocument(); }); - test('render name', () => { - const mappedField = { - name: 'customer', - info: [], - excluded: false, - kbnType: 'string', - type: 'keyword', - isMapped: true, - isUserEditable: true, - hasRuntime: false, - }; - - expect(renderFieldName(mappedField)).toMatchSnapshot(); - - const runtimeField = { - name: 'customer', - info: [], - excluded: false, - kbnType: 'string', - type: 'keyword', - isMapped: false, - isUserEditable: true, + it('render name', async () => { + const mappedField = createIndexedField({ name: 'customer' }); + + const { unmount } = renderWithI18n(renderFieldName(mappedField)); + + expect(await screen.findByText('String')).toBeVisible(); + expect(screen.getByTestId('field-name-customer')).toHaveTextContent('customer'); + + unmount(); + + const runtimeField = createIndexedField({ hasRuntime: true, - }; + isMapped: false, + name: 'customer', + }); + + renderWithI18n(renderFieldName(runtimeField)); - expect(renderFieldName(runtimeField)).toMatchSnapshot(); + expect(await screen.findByText('String')).toBeVisible(); + expect(screen.getByTestId('field-name-customer')).toHaveTextContent('customer'); + expect(screen.getByText('Info')).toBeVisible(); }); - test('render conflict summary modal ', () => { - expect( + it('render conflict summary modal ', () => { + renderWithI18n( getConflictModalContent({ closeFn: () => {}, - fieldName: 'message', conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] }, + fieldName: 'message', }) - ).toMatchSnapshot(); - }); + ); - test('showDelete', () => { - const runtimeFields = [ - { - name: 'customer', - info: [], - excluded: false, - kbnType: 'string', - type: 'keyword', - isMapped: false, - isUserEditable: true, - hasRuntime: true, - runtimeField: { - type: 'keyword', - }, - }, - { - name: 'thing', - info: [], - excluded: false, - kbnType: 'string', - type: 'keyword', - isMapped: false, - isUserEditable: true, - hasRuntime: true, - runtimeField: { - type: 'composite', - }, - }, - ] as IndexedFieldItem[]; + expect(screen.getByText('This field has a type conflict')).toBeVisible(); + expect(screen.getByText(/The type of the/)).toHaveTextContent('message'); + expect(screen.getByText('keyword')).toBeVisible(); + expect(screen.getByText('index_a')).toBeVisible(); + expect(screen.getByText('long')).toBeVisible(); + expect(screen.getByText('index_b')).toBeVisible(); + expect(screen.getByText('Close')).toBeVisible(); + }); + it('showDelete', () => { // indexed field expect(showDelete(items[0])).toBe(false); // runtime field - primitive type - expect(showDelete(runtimeFields[0])).toBe(true); - // runtime field - composite type - expect(showDelete(runtimeFields[1])).toBe(false); + expect(showDelete(primitiveRuntimeField)).toBe(true); + // runtime field - composite subfield + expect(showDelete(compositeRuntimeField)).toBe(false); + // runtime field - composite definition + expect(showDelete(compositeRuntimeDefinition)).toBe(true); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 3079c84c21410..54eedada072cf 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -7,69 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { RuntimeField } from '@kbn/data-views-plugin/common'; import React from 'react'; -import type { ShallowWrapper } from 'enzyme'; -import { shallow } from 'enzyme'; import { coreMock } from '@kbn/core/public/mocks'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import { createStubDataView } from '@kbn/data-views-plugin/public/data_views/data_view.stub'; import { DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; -import { IndexedFieldsTable } from './indexed_fields_table'; import { getFieldInfo } from '../../utils'; -import type { RuntimeField } from '@kbn/data-views-plugin/common'; - -jest.mock('@elastic/eui', () => ({ - EuiFlexGroup: 'eui-flex-group', - EuiFlexItem: 'eui-flex-item', - EuiIcon: 'eui-icon', - EuiInMemoryTable: 'eui-in-memory-table', -})); - -jest.mock('./components/table', () => ({ - // Note: this seems to fix React complaining about non lowercase attributes - Table: () => { - return 'table'; - }, -})); - -const helpers = { - editField: (_fieldName: string) => {}, - deleteField: (_fieldName: string[]) => {}, - // getFieldInfo handles non rollups as well - getFieldInfo, -}; - -const indexPattern = { - getNonScriptedFields: () => fields, - getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), -} as unknown as DataView; - -const rollupIndexPattern = { - type: DataViewType.ROLLUP, - typeMeta: { - params: { - 'rollup-index': 'rollup', - }, - aggs: { - date_histogram: { - timestamp: { - agg: 'date_histogram', - fixed_interval: '30s', - delay: '30s', - time_zone: 'UTC', - }, - }, - terms: { Elastic: { agg: 'terms' } }, - histogram: { amount: { agg: 'histogram', interval: 5 } }, - avg: { amount: { agg: 'avg' } }, - max: { amount: { agg: 'max' } }, - min: { amount: { agg: 'min' } }, - sum: { amount: { agg: 'sum' } }, - value_count: { amount: { agg: 'value_count' } }, - }, - }, - getNonScriptedFields: () => fields, - getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), -} as unknown as DataView; +import { IndexedFieldsTable } from './indexed_fields_table'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; const mockFieldToIndexPatternField = ( spec: Record @@ -78,22 +24,37 @@ const mockFieldToIndexPatternField = ( }; const runtimeField: RuntimeField = { type: 'long', script: { source: "emit('Hello');" } }; + const fields = [ { name: 'Elastic', displayName: 'Elastic', searchable: true, esTypes: ['keyword'], + type: 'string', + isUserEditable: true, + }, + { + name: 'timestamp', + displayName: 'timestamp', + esTypes: ['date'], + type: 'date', isUserEditable: true, }, - { name: 'timestamp', displayName: 'timestamp', esTypes: ['date'], isUserEditable: true }, { name: 'conflictingField', displayName: 'conflictingField', esTypes: ['keyword', 'long'], + type: 'conflict', + isUserEditable: true, + }, + { + name: 'amount', + displayName: 'amount', + esTypes: ['long'], + type: 'number', isUserEditable: true, }, - { name: 'amount', displayName: 'amount', esTypes: ['long'], isUserEditable: true }, { name: 'runtime', displayName: 'runtime', @@ -103,137 +64,147 @@ const fields = [ }, ].map(mockFieldToIndexPatternField); -const startServices = coreMock.createStart(); +const fieldsMap = Object.fromEntries(fields.map((field) => [field.name, field.spec])); + +const helpers = { + editField: (_fieldName: string) => {}, + deleteField: (_fieldName: string[]) => {}, + // getFieldInfo handles non rollups as well + getFieldInfo, +}; + +const indexPattern = createStubDataView({ + spec: { + title: 'test-data-view', + fields: fieldsMap, + }, +}); const mockedServices = { - userEditPermission: false, openModal: () => ({ onClose: new Promise(() => {}), close: async () => {} }), - theme: {} as any, + theme: {}, + userEditPermission: false, +}; + +const rollupIndexPattern = createStubDataView({ + spec: { + title: 'rollup-data-view', + fields: fieldsMap, + type: DataViewType.ROLLUP, + typeMeta: { + params: { + rollup_index: 'rollup', + }, + aggs: { + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: '30s', + delay: '30s', + time_zone: 'UTC', + }, + }, + terms: { Elastic: { agg: 'terms' } }, + histogram: { amount: { agg: 'histogram', interval: 5 } }, + avg: { amount: { agg: 'avg' } }, + max: { amount: { agg: 'max' } }, + min: { amount: { agg: 'min' } }, + sum: { amount: { agg: 'sum' } }, + value_count: { amount: { agg: 'value_count' } }, + }, + }, + }, +}); + +const startServices = coreMock.createStart(); + +// EUI field icons load asynchronously, so each test waits for a visible icon before +// asserting the table contents. +const expectFieldTypeIconVisible = async (label: string) => { + expect((await screen.findAllByTitle(label))[0]).toBeVisible(); +}; + +const expectHiddenFields = (fieldNames: string[]) => { + fieldNames.forEach((fieldName) => { + expect(screen.queryByText(fieldName)).not.toBeInTheDocument(); + }); +}; + +const expectVisibleFields = (fieldNames: string[]) => { + fieldNames.forEach((fieldName) => { + expect(screen.getByText(fieldName)).toBeVisible(); + }); +}; + +const renderIndexedFieldsTable = ( + props: Partial> +) => { + renderWithI18n( + { + return () => false; + }} + helpers={helpers} + indexedFieldTypeFilter={[]} + indexPattern={indexPattern} + schemaFieldTypeFilter={[]} + startServices={startServices} + {...mockedServices} + {...props} + /> + ); }; describe('IndexedFieldsTable', () => { - test('should render normally', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( - { - return () => false; - }} - indexedFieldTypeFilter={[]} - schemaFieldTypeFilter={[]} - fieldFilter="" - compositeRuntimeFields={{}} - startServices={startServices} - {...mockedServices} - /> - ); - - await new Promise((resolve) => process.nextTick(resolve)); - component.update(); - - expect({ items: component.props().children.props.items }).toMatchSnapshot(); + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); // Silent EUI warnings during tests }); - test('should filter based on the query bar', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( - { - return () => false; - }} - indexedFieldTypeFilter={[]} - schemaFieldTypeFilter={[]} - fieldFilter="" - compositeRuntimeFields={{}} - startServices={startServices} - {...mockedServices} - /> - ); - - await new Promise((resolve) => process.nextTick(resolve)); - component.setProps({ fieldFilter: 'Elast' }); - component.update(); - - expect({ items: component.props().children.props.items }).toMatchSnapshot(); + afterEach(() => { + jest.restoreAllMocks(); }); - test('should filter based on the type filter', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( - { - return () => false; - }} - indexedFieldTypeFilter={[]} - schemaFieldTypeFilter={[]} - fieldFilter="" - compositeRuntimeFields={{}} - startServices={startServices} - {...mockedServices} - /> - ); - - await new Promise((resolve) => process.nextTick(resolve)); - component.setProps({ indexedFieldTypeFilter: ['date'] }); - component.update(); - - expect({ items: component.props().children.props.items }).toMatchSnapshot(); + it('should render normally', async () => { + renderIndexedFieldsTable({}); + + await expectFieldTypeIconVisible('Keyword'); + expect(screen.getByTestId('tableHeaderCell_displayName_0')).toBeVisible(); + expectVisibleFields(['Elastic', 'timestamp', 'conflictingField', 'amount', 'runtime']); }); - test('should filter based on the schema filter', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( - { - return () => false; - }} - indexedFieldTypeFilter={[]} - schemaFieldTypeFilter={[]} - fieldFilter="" - compositeRuntimeFields={{}} - startServices={startServices} - {...mockedServices} - /> - ); - - await new Promise((resolve) => process.nextTick(resolve)); - component.setProps({ schemaFieldTypeFilter: ['runtime'] }); - component.update(); - - expect({ items: component.props().children.props.items }).toMatchSnapshot(); + it('should filter based on the query bar', async () => { + renderIndexedFieldsTable({ fieldFilter: 'Elast' }); + + await expectFieldTypeIconVisible('Keyword'); + expectVisibleFields(['Elastic']); + expectHiddenFields(['timestamp', 'conflictingField', 'amount', 'runtime']); + }); + + it('should filter based on the type filter', async () => { + renderIndexedFieldsTable({ indexedFieldTypeFilter: ['date'] }); + + await expectFieldTypeIconVisible('Date'); + expectVisibleFields(['timestamp']); + expectHiddenFields(['Elastic', 'conflictingField', 'amount', 'runtime']); + }); + + it('should filter based on the schema filter', async () => { + renderIndexedFieldsTable({ schemaFieldTypeFilter: ['runtime'] }); + + await expectFieldTypeIconVisible('Number'); + expectVisibleFields(['runtime']); + expectHiddenFields(['Elastic', 'timestamp', 'conflictingField', 'amount']); }); describe('IndexedFieldsTable with rollup index pattern', () => { - test('should render normally', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( - { - return () => false; - }} - indexedFieldTypeFilter={[]} - schemaFieldTypeFilter={[]} - fieldFilter="" - compositeRuntimeFields={{}} - startServices={startServices} - {...mockedServices} - /> - ); - - await new Promise((resolve) => process.nextTick(resolve)); - component.update(); - - expect({ items: component.props().children.props.items }).toMatchSnapshot(); + it('should render normally', async () => { + renderIndexedFieldsTable({ indexPattern: rollupIndexPattern }); + + await expectFieldTypeIconVisible('Keyword'); + expectVisibleFields(['Elastic', 'timestamp', 'conflictingField', 'amount', 'runtime']); }); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap deleted file mode 100644 index 19172b9866085..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap +++ /dev/null @@ -1,185 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = ` - -
- - -
- -`; - -exports[`ScriptedFieldsTable should filter based on the query bar 1`] = ` - -
- - -
- -`; - -exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = ` - -
- - -
- -`; - -exports[`ScriptedFieldsTable should render normally 1`] = ` - -
- - -
- -`; - -exports[`ScriptedFieldsTable should show a delete modal 1`] = ` - -
- - -
- - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap deleted file mode 100644 index 432a526ffcafc..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CallOuts should render normally 1`] = ` - - - - } - > -

- - - , - } - } - /> -

-
-
-`; - -exports[`CallOuts should render without any call outs 1`] = `""`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx index 0e4658ea7f895..720737ff28ac2 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx @@ -8,27 +8,35 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - import { CallOuts } from '.'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; describe('CallOuts', () => { - test('should render normally', () => { - const component = shallow( + it('should render normally', () => { + renderWithI18n( ); - expect(component).toMatchSnapshot(); + expect(screen.getByText('Deprecated languages in use')).toBeVisible(); + expect( + screen.getByText('The following deprecated languages are in use: php.', { exact: false }) + ).toBeVisible(); + expect(screen.getByRole('link', { name: 'Painless' })).toHaveAttribute( + 'href', + 'http://www.elastic.co/painlessDocs' + ); }); - test('should render without any call outs', () => { - const component = shallow( + it('should render without any call outs', () => { + const { container } = renderWithI18n( ); - expect(component).toMatchSnapshot(); + expect(container).toBeEmptyDOMElement(); + expect(screen.queryByText('Deprecated languages in use')).not.toBeInTheDocument(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap deleted file mode 100644 index f83f6976f0419..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DeleteScritpedFieldConfirmationModal should render normally 1`] = ` - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx index a1aa33a0e885f..03ea124025220 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx @@ -8,20 +8,20 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - import { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; describe('DeleteScritpedFieldConfirmationModal', () => { - test('should render normally', () => { - const component = shallow( + it('should render normally', () => { + renderWithI18n( {}} - hideDeleteConfirmationModal={() => {}} + deleteField={jest.fn()} + field={{ lang: '', name: '', script: '' }} + hideDeleteConfirmationModal={jest.fn()} /> ); - expect(component).toMatchSnapshot(); + expect(screen.getByText("Delete scripted field ''?")).toBeVisible(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap deleted file mode 100644 index 7905ba607b07b..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ /dev/null @@ -1,108 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Table should render normally 1`] = ` - -`; - -exports[`Table should render the format 1`] = ` - - string - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx index 539514a8fac8e..32a6cc160f6f5 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -7,121 +7,156 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { ScriptedFieldItem } from '../../types'; import React from 'react'; -import { shallow } from 'enzyme'; - +import { createStubDataView } from '@kbn/data-views-plugin/public/data_views/data_view.stub'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; import { TableWithoutPersist as Table } from './table'; -import type { ScriptedFieldItem } from '../../types'; -import type { DataView } from '@kbn/data-views-plugin/public'; - -const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); - -const items: ScriptedFieldItem[] = [ - { name: '1', lang: 'painless', script: '', isUserEditable: true }, - { name: '2', lang: 'painless', script: '', isUserEditable: false }, -]; +import { userEvent } from '@testing-library/user-event'; + +const createScriptedField = ({ + displayName, + ...field +}: ScriptedFieldItem & { displayName: string }) => ({ + displayName, + ...field, +}); -const baseProps = { +const baseProps: Pick, 'euiTablePersist'> = { euiTablePersist: { + onTableChange: jest.fn(), pageSize: 10, - onTableChange: () => {}, - sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + sorting: { sort: { direction: 'asc', field: 'name' } }, }, }; -describe('Table', () => { - let indexPattern: DataView; - - beforeEach(() => { - indexPattern = getIndexPatternMock({ - fieldFormatMap: { - Elastic: { - type: { - title: 'string', - }, +const indexPattern = Object.assign( + createStubDataView({ + spec: { + title: 'test-data-view', + }, + }), + { + fieldFormatMap: { + Elastic: { + type: { + title: 'string', }, }, - }); + }, + } +); + +const items = [ + createScriptedField({ + displayName: 'Elastic', + isUserEditable: true, + lang: 'painless', + name: 'Elastic', + script: "emit('one')", + }), + createScriptedField({ + displayName: 'ReadonlyScriptedField', + isUserEditable: false, + lang: 'painless', + name: 'ReadonlyScriptedField', + script: "emit('two')", + }), +]; + +const getActionButton = (fieldName: string, buttonIndex: number) => { + const button = getRowByText(fieldName).querySelectorAll('button')[buttonIndex]; + + if (!button) { + throw new Error(`Unable to find action button ${buttonIndex} for field ${fieldName}`); + } + + return button; +}; + +const getRowByText = (text: string) => { + const row = screen.getByText(text).closest('tr'); + + if (!row) throw new Error(`Unable to find row for ${text}`); + + return row; +}; + +const renderTable = ({ + deleteField = jest.fn(), + editField = jest.fn(), + tableItems = items, +}: { + deleteField?: React.ComponentProps['deleteField']; + editField?: React.ComponentProps['editField']; + tableItems?: React.ComponentProps['items']; +} = {}) => { + renderWithI18n( +
+ ); + + return { deleteField, editField }; +}; + +describe('Table', () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); // Silent EUI warnings during tests + }); + + afterEach(() => { + jest.restoreAllMocks(); }); - test('should render normally', () => { - const component = shallow( -
{}} - deleteField={() => {}} - /> - ); - - expect(component).toMatchSnapshot(); + it('should render normally', () => { + renderTable(); + + expect(screen.getByText('Elastic')).toBeVisible(); + expect(screen.getByText('ReadonlyScriptedField')).toBeVisible(); + expect(screen.getAllByText('painless')).toHaveLength(2); + expect(screen.getByText("emit('one')")).toBeVisible(); + expect(screen.getByText("emit('two')")).toBeVisible(); }); - test('should render the format', () => { - const component = shallow( -
{}} - deleteField={() => {}} - /> - ); - - const formatTableCell = shallow(component.prop('columns')[3].render('Elastic')); - expect(formatTableCell).toMatchSnapshot(); + it('should render the format', () => { + renderTable(); + + expect(getRowByText('Elastic')).toHaveTextContent('string'); }); - test('should allow edits', () => { + it('should allow edits', async () => { + const user = userEvent.setup(); const editField = jest.fn(); - const component = shallow( -
{}} - /> - ); - - // Click the delete button - component.prop('columns')[4].actions[0].onClick(); - expect(editField).toBeCalled(); + renderTable({ editField }); + + await user.click(getActionButton('Elastic', 0)); + + expect(editField).toHaveBeenCalled(); }); - test('should allow deletes', () => { + it('should allow deletes', async () => { + const user = userEvent.setup(); const deleteField = jest.fn(); - const component = shallow( -
{}} - deleteField={deleteField} - /> - ); - - // Click the delete button - component.prop('columns')[4].actions[1].onClick(); - expect(deleteField).toBeCalled(); + renderTable({ deleteField }); + + await user.click(getActionButton('Elastic', 1)); + + expect(deleteField).toHaveBeenCalled(); }); - test('should not allow edit or deletion for user with only read access', () => { - const component = shallow( -
{}} - deleteField={() => {}} - /> - ); - const editAvailable = component.prop('columns')[4].actions[0].available(items[1]); - const deleteAvailable = component.prop('columns')[4].actions[1].available(items[1]); - expect(editAvailable).toBeFalsy(); - expect(deleteAvailable).toBeFalsy(); + it('should not allow edit or deletion for user with only read access', () => { + renderTable({ tableItems: [items[1]] }); + + expect(screen.getByText('ReadonlyScriptedField')).toBeVisible(); + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + expect(screen.queryByText('Delete')).not.toBeInTheDocument(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index 5e1943aa894db..0c16b57fd14cd 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -7,214 +7,209 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; import React from 'react'; -import type { ShallowWrapper } from 'enzyme'; -import { shallow } from 'enzyme'; - +import userEvent from '@testing-library/user-event'; +import { createStubDataView } from '@kbn/data-views-plugin/public/data_views/data_view.stub'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen, within } from '@testing-library/react'; import { ScriptedFieldsTable } from '.'; -import type { DataView } from '@kbn/data-views-plugin/public'; -jest.mock('@elastic/eui', () => ({ - EuiTitle: 'eui-title', - EuiText: 'eui-text', - EuiHorizontalRule: 'eui-horizontal-rule', - EuiSpacer: 'eui-spacer', - EuiCallOut: 'eui-call-out', - EuiLink: 'eui-link', - EuiOverlayMask: 'eui-overlay-mask', - EuiConfirmModal: 'eui-confirm-modal', - Comparators: { - property: () => {}, - default: () => {}, - }, -})); -jest.mock('./components/header', () => ({ Header: 'header' })); -jest.mock('./components/call_outs', () => ({ CallOuts: 'call-outs' })); -jest.mock('./components/table', () => ({ - // Note: this seems to fix React complaining about non lowercase attributes - Table: () => { - return 'table'; - }, +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: () => ({ + services: { + docLinks: { + links: { + indexPatterns: { + runtimeFields: '#', + }, + query: { + queryESQL: '#', + }, + }, + }, + }, + }), })); const helpers = { - redirectToRoute: () => {}, - getRouteHref: () => '#', + getRouteHref: jest.fn(), + redirectToRoute: jest.fn(), }; -const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); +const createDataViewWithScriptedFields = (scriptedFields: FieldSpec[]) => + createStubDataView({ + spec: { + title: 'test-data-view', + fields: Object.fromEntries(scriptedFields.map((field) => [field.name, field])), + }, + }); + +const createScriptedField = ({ + name, + lang = 'painless', + script, +}: { + name: string; + lang?: string; + script: string; +}): FieldSpec => ({ + aggregatable: false, + lang, + name, + script, + scripted: true, + searchable: false, + type: 'number', +}); + +const getDeleteButtonForField = (fieldName: string) => + within(screen.getByRole('row', { name: new RegExp(fieldName) })).getAllByRole('button', { + name: 'Edit', + })[1]; describe('ScriptedFieldsTable', () => { let indexPattern: DataView; beforeEach(() => { - indexPattern = getIndexPatternMock({ - getScriptedFields: () => [ - { isUserEditable: true, name: 'ScriptedField', lang: 'painless', script: 'x++' }, - { - isUserEditable: false, - name: 'JustATest', - lang: 'painless', - script: 'z++', - }, - ], - }) as DataView; + indexPattern = createDataViewWithScriptedFields([ + createScriptedField({ name: 'ScriptedField', script: 'x++' }), + createScriptedField({ name: 'JustATest', script: 'z++' }), + ]); + jest.spyOn(console, 'warn').mockImplementation(() => {}); // Silent EUI warnings during tests + }); + + afterEach(() => { + jest.restoreAllMocks(); }); - test('should render normally', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow< - typeof ScriptedFieldsTable - >( + it('should render normally', () => { + renderWithI18n( {}} - userEditPermission={false} scriptedFieldLanguageFilter={[]} + userEditPermission={false} /> ); - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - expect(component).toMatchSnapshot(); + expect(screen.getByText('Scripted fields are deprecated')).toBeVisible(); + expect(screen.getByTestId('tableHeaderCell_displayName_0')).toBeVisible(); + expect(screen.getByText('ScriptedField')).toBeVisible(); + expect(screen.getByText('JustATest')).toBeVisible(); + expect(screen.queryByText('Deprecated languages in use')).not.toBeInTheDocument(); }); - test('should filter based on the query bar', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( + it('should filter based on the query bar', () => { + renderWithI18n( {}} - userEditPermission={false} scriptedFieldLanguageFilter={[]} + userEditPermission={false} /> ); - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - component.setProps({ fieldFilter: 'Just' }); - component.update(); - - expect(component).toMatchSnapshot(); + expect(screen.queryByText('ScriptedField')).not.toBeInTheDocument(); + expect(screen.getByText('JustATest')).toBeVisible(); }); - test('should filter based on the lang filter', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow< - typeof ScriptedFieldsTable - >( + it('should filter based on the lang filter', () => { + const BAD_LANG = 'somethingElse'; + const indexPatternWithDeprecatedLang = createDataViewWithScriptedFields([ + createScriptedField({ name: 'ScriptedField', script: 'x++' }), + createScriptedField({ name: 'JustATest', script: 'z++' }), + createScriptedField({ name: 'Bad', lang: BAD_LANG, script: 'z++' }), + ]); + + renderWithI18n( [ - { isUserEditable: true, name: 'ScriptedField', lang: 'painless', script: 'x++' }, - { isUserEditable: true, name: 'JustATest', lang: 'painless', script: 'z++' }, - { isUserEditable: true, name: 'Bad', lang: 'somethingElse', script: 'z++' }, - ], - }) as DataView - } - painlessDocLink={'painlessDoc'} helpers={helpers} + indexPattern={indexPatternWithDeprecatedLang} + painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={['painless']} userEditPermission={false} - scriptedFieldLanguageFilter={[]} /> ); - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - component.setProps({ scriptedFieldLanguageFilter: ['painless'] }); - component.update(); - - expect(component).toMatchSnapshot(); + expect(screen.getByText('Deprecated languages in use')).toBeVisible(); + expect( + screen.getByText(`The following deprecated languages are in use: ${BAD_LANG}.`, { + exact: false, + }) + ).toBeVisible(); + expect(screen.getByText('ScriptedField')).toBeVisible(); + expect(screen.getByText('JustATest')).toBeVisible(); + expect(screen.queryByText('Bad')).not.toBeInTheDocument(); }); - test('should hide the table if there are no scripted fields', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( + it('should show an empty table if there are no scripted fields', () => { + renderWithI18n( [], - }) as DataView - } - painlessDocLink={'painlessDoc'} helpers={helpers} + indexPattern={createDataViewWithScriptedFields([])} + painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} - userEditPermission={false} scriptedFieldLanguageFilter={[]} + userEditPermission={false} /> ); - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - expect(component).toMatchSnapshot(); + expect(screen.getByTestId('tableHeaderCell_displayName_0')).toBeVisible(); + expect(screen.getByText('No items found')).toBeVisible(); + expect(screen.queryByText('ScriptedField')).not.toBeInTheDocument(); + expect(screen.queryByText('JustATest')).not.toBeInTheDocument(); }); - test('should show a delete modal', async () => { - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow< - typeof ScriptedFieldsTable - >( + it('should show a delete modal', async () => { + const user = userEvent.setup(); + + renderWithI18n( {}} - userEditPermission={false} scriptedFieldLanguageFilter={[]} + userEditPermission={true} /> ); - await component.update(); // Fire `componentWillMount()` - // @ts-expect-error lang is not valid - component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' }); - await component.update(); + await user.click(getDeleteButtonForField('ScriptedField')); - // Ensure the modal is visible - expect(component).toMatchSnapshot(); + expect(screen.getByText("Delete scripted field 'ScriptedField'?")).toBeVisible(); }); - test('should delete a field', async () => { - const removeScriptedField = jest.fn(); - const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow< - typeof ScriptedFieldsTable - >( + it('should delete a field', async () => { + const user = userEvent.setup(); + + const removeScriptedFieldSpy = jest.fn(); + const saveIndexPatternSpy = jest.fn(); + jest.spyOn(indexPattern, 'removeScriptedField').mockImplementation(removeScriptedFieldSpy); + + renderWithI18n( {}} - userEditPermission={false} + saveIndexPattern={saveIndexPatternSpy} scriptedFieldLanguageFilter={[]} + userEditPermission={true} /> ); - await component.update(); // Fire `componentWillMount()` - // @ts-expect-error - component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' }); - - await component.update(); - // @ts-expect-error - await component.instance().deleteField(); - await component.update(); + await user.click(getDeleteButtonForField('ScriptedField')); + expect(screen.getByText("Delete scripted field 'ScriptedField'?")).toBeVisible(); + await user.click(screen.getByTestId('confirmModalConfirmButton')); - expect(removeScriptedField).toBeCalled(); + expect(removeScriptedFieldSpy).toHaveBeenCalledWith('ScriptedField'); + expect(saveIndexPatternSpy).toHaveBeenCalledWith(indexPattern); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap deleted file mode 100644 index 6a2b208c47987..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap +++ /dev/null @@ -1,280 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SourceFiltersTable should add a filter 1`] = ` - -
- - -
- -`; - -exports[`SourceFiltersTable should filter based on the query bar 1`] = ` - -
- - -
- -`; - -exports[`SourceFiltersTable should remove a filter 1`] = ` - -
- - -
- -`; - -exports[`SourceFiltersTable should render normally 1`] = ` - -
- - -
- -`; - -exports[`SourceFiltersTable should should a loading indicator when saving 1`] = ` - -
- - -
- -`; - -exports[`SourceFiltersTable should show a delete modal 1`] = ` - -
- - -
- - -`; - -exports[`SourceFiltersTable should update a filter 1`] = ` - -
- - -
- -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap deleted file mode 100644 index d3181121b75e3..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header should render normally 1`] = ` - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - titleProps={ - Object { - "id": "generated-id", - } - } -/> -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx index 7296a77b50691..0d25db80d1f0e 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx @@ -8,13 +8,13 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - import { DeleteFilterConfirmationModal } from './confirmation_modal'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; describe('Header', () => { - test('should render normally', () => { - const component = shallow( + it('should render normally', () => { + renderWithI18n( {}} @@ -22,6 +22,6 @@ describe('Header', () => { /> ); - expect(component).toMatchSnapshot(); + expect(screen.getByText("Delete field filter 'test'?")).toBeVisible(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap deleted file mode 100644 index 97486302553f5..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header should render normally 1`] = ` - - -

- -

-

- -

-
- -
-`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/header.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/header.test.tsx index 83e4672cff9e4..132eb31b1cec5 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/header.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/header/header.test.tsx @@ -8,14 +8,23 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - import { Header } from '.'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; describe('Header', () => { - test('should render normally', () => { - const component = shallow(
); + it('should render normally', () => { + renderWithI18n(
); - expect(component).toMatchSnapshot(); + expect( + screen.getByText( + 'Field filters can be used to exclude one or more fields when fetching a document. This happens when viewing a document in the Discover app, or with a table displaying results from a Discover session in the Dashboard app. If you have documents with large or unimportant fields you may benefit from filtering those out at this lower level.' + ) + ).toBeVisible(); + expect( + screen.getByText( + 'Note that multi-fields will incorrectly appear as matches in the table below. These filters only actually apply to fields in the original source document, so matching multi-fields are not actually being filtered.' + ) + ).toBeVisible(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index 89a65123c8d3d..32571d45115c3 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -57,7 +57,7 @@ describe('Table', () => { jest.clearAllMocks(); }); - test('renders table headers and rows', () => { + it('renders table headers and rows', () => { renderTable({ isSaving: true }); expect(screen.getByRole('table')).toBeInTheDocument(); @@ -66,7 +66,7 @@ describe('Table', () => { expect(screen.getByText('tim*')).toBeInTheDocument(); }); - test('renders filter matches when found', () => { + it('renders filter matches when found', () => { renderTable({ indexPattern: getIndexPatternMock(['time', 'value']), fieldWildcardMatcher: () => () => true, @@ -75,7 +75,7 @@ describe('Table', () => { expect(screen.getByText('time, value')).toBeInTheDocument(); }); - test('renders no matches message when there are no matches', () => { + it('renders no matches message when there are no matches', () => { renderTable({ indexPattern: getIndexPatternMock(['value']), }); @@ -86,7 +86,7 @@ describe('Table', () => { }); describe('editing', () => { - test('shows input and save button after entering edit mode', async () => { + it('shows input and save button after entering edit mode', async () => { const user = userEvent.setup(); renderTable(); @@ -96,7 +96,7 @@ describe('Table', () => { expect(screen.getByTestId('save_filter-tim*')).toBeInTheDocument(); }); - test('updates matches dynamically as input value changes', async () => { + it('updates matches dynamically as input value changes', async () => { const user = userEvent.setup(); renderTable({ indexPattern: getIndexPatternMock(['time', 'value']), @@ -110,7 +110,7 @@ describe('Table', () => { expect(screen.queryByText('time, value')).not.toBeInTheDocument(); }); - test('saves edited filter when save icon is clicked and exits edit mode', async () => { + it('saves edited filter when save icon is clicked and exits edit mode', async () => { const user = userEvent.setup(); const saveFilter = jest.fn(); renderTable({ saveFilter }); @@ -125,7 +125,7 @@ describe('Table', () => { }); }); - test('allows deletes', async () => { + it('allows deletes', async () => { const user = userEvent.setup(); const deleteFilter = jest.fn(); renderTable({ deleteFilter }); @@ -135,7 +135,7 @@ describe('Table', () => { expect(deleteFilter).toHaveBeenCalledWith({ clientId: '1', value: 'tim*' }); }); - test('saves when in edit mode and Enter key is pressed', async () => { + it('saves when in edit mode and Enter key is pressed', async () => { const user = userEvent.setup(); const saveFilter = jest.fn(); renderTable({ saveFilter }); @@ -150,7 +150,7 @@ describe('Table', () => { expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); }); - test('cancels when in edit mode and Escape key is pressed', async () => { + it('cancels when in edit mode and Escape key is pressed', async () => { const user = userEvent.setup(); const saveFilter = jest.fn(); renderTable({ saveFilter }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx index 4117d58051817..619f97609a635 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx @@ -8,168 +8,175 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - +import { createStubDataView } from '@kbn/data-views-plugin/public/data_views/data_view.stub'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen, waitFor } from '@testing-library/react'; import { SourceFiltersTable } from './source_filters_table'; -import type { DataView } from '@kbn/data-views-plugin/public'; - -jest.mock('@elastic/eui', () => ({ - EuiButton: 'eui-button', - EuiTitle: 'eui-title', - EuiText: 'eui-text', - EuiHorizontalRule: 'eui-horizontal-rule', - EuiSpacer: 'eui-spacer', - EuiCallOut: 'eui-call-out', - EuiLink: 'eui-link', - EuiOverlayMask: 'eui-overlay-mask', - EuiConfirmModal: 'eui-confirm-modal', - EuiLoadingSpinner: 'eui-loading-spinner', - Comparators: { - property: () => {}, - default: () => {}, - }, -})); - -jest.mock('./components/header', () => ({ Header: 'header' })); -jest.mock('./components/table', () => ({ - // Note: this seems to fix React complaining about non lowercase attributes - Table: () => { - return 'table'; - }, -})); - -const getIndexPatternMock = (mockedFields: any = {}) => - ({ - sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], - ...mockedFields, - } as DataView); +import { userEvent } from '@testing-library/user-event'; + +const createDataView = ( + sourceFilters = [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }] +) => + createStubDataView({ + spec: { + fields: { + time: { name: 'time', type: 'date', searchable: true, aggregatable: true }, + name: { name: 'name', type: 'string', searchable: true, aggregatable: true }, + age: { name: 'age', type: 'number', searchable: true, aggregatable: true }, + }, + sourceFilters, + title: 'test-data-view', + }, + }); + +const fieldWildcardMatcher = (filters: string[]) => { + const [query = ''] = filters; + const normalizedQuery = query.replace('*', ''); + + return (field: string) => field.includes(normalizedQuery); +}; + +const getFilterActionButton = (filterValue: string, buttonIndex: number) => { + const button = getFilterRow(filterValue).querySelectorAll('button')[buttonIndex]; + + if (!button) { + throw new Error(`Unable to find action button ${buttonIndex} for filter ${filterValue}`); + } + + return button; +}; + +const getFilterRow = (filterValue: string) => { + const row = screen.getByText(filterValue).closest('tr'); + + if (!row) throw new Error(`Unable to find row for filter ${filterValue}`); + + return row; +}; + +const renderSourceFiltersTable = ({ + filterFilter = '', + indexPattern = createDataView(), + saveIndexPattern = jest.fn(async () => {}), +}: Partial> = {}) => { + renderWithI18n( + + ); + + return { indexPattern, saveIndexPattern }; +}; describe('SourceFiltersTable', () => { - test('should render normally', () => { - const component = shallow( - {}} - filterFilter={''} - saveIndexPattern={async () => {}} - /> - ); - - expect(component).toMatchSnapshot(); + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); // Silent EUI warnings during tests }); - test('should filter based on the query bar', () => { - const component = shallow( - {}} - filterFilter={''} - saveIndexPattern={async () => {}} - /> - ); - - component.setProps({ filterFilter: 'ti' }); - expect(component).toMatchSnapshot(); + afterEach(() => { + jest.restoreAllMocks(); }); - test('should should a loading indicator when saving', () => { - const component = shallow( - {}} - saveIndexPattern={async () => {}} - /> - ); - - component.setState({ isSaving: true }); - expect(component).toMatchSnapshot(); + it('should render normally', () => { + renderSourceFiltersTable(); + + expect(screen.getByText('time*')).toBeVisible(); + expect(screen.getByText('nam*')).toBeVisible(); + expect(screen.getByText('age*')).toBeVisible(); + expect(screen.getByTestId('fieldFilterInput')).toBeVisible(); + expect(screen.getByText('Add')).toBeVisible(); }); - test('should show a delete modal', () => { - const component = shallow( - {}} - saveIndexPattern={async () => {}} - /> - ); - - component.instance().startDeleteFilter({ value: 'tim*', clientId: 1 }); - component.update(); // We are not calling `.setState` directly so we need to re-render - expect(component).toMatchSnapshot(); + it('should filter based on the query bar', () => { + renderSourceFiltersTable({ filterFilter: 'ti' }); + + expect(screen.getByText('time*')).toBeVisible(); + expect(screen.queryByText('nam*')).not.toBeInTheDocument(); + expect(screen.queryByText('age*')).not.toBeInTheDocument(); }); - test('should remove a filter', async () => { + it('should show a loading indicator when saving', async () => { + const user = userEvent.setup(); const saveIndexPattern = jest.fn(async () => {}); - const component = shallow( - {}} - saveIndexPattern={saveIndexPattern} - /> - ); - - component.instance().startDeleteFilter({ value: 'tim*', clientId: 1 }); - component.update(); // We are not calling `.setState` directly so we need to re-render - await component.instance().deleteFilter(); - component.update(); // We are not calling `.setState` directly so we need to re-render - expect(saveIndexPattern).toBeCalled(); - expect(component).toMatchSnapshot(); + renderSourceFiltersTable({ + indexPattern: createDataView([{ value: 'tim*' }]), + saveIndexPattern, + }); + + await user.type(screen.getByTestId('fieldFilterInput'), 'na*'); + await user.click(screen.getByText('Add')); + + expect(saveIndexPattern).toHaveBeenCalled(); + expect(screen.getByText('tim*')).toBeVisible(); + }); + + it('should show a delete modal', async () => { + const user = userEvent.setup(); + renderSourceFiltersTable({ indexPattern: createDataView([{ value: 'tim*' }]) }); + + await user.click(getFilterActionButton('tim*', 1)); + + expect(screen.getByText("Delete field filter 'tim*'?")).toBeVisible(); + }); + + it('should remove a filter', async () => { + const user = userEvent.setup(); + const saveIndexPattern = jest.fn(async () => {}); + + const { indexPattern } = renderSourceFiltersTable({ + indexPattern: createDataView([{ value: 'tim*' }, { value: 'na*' }]), + saveIndexPattern, + }); + await user.click(getFilterActionButton('tim*', 1)); + await user.click(screen.getByTestId('confirmModalConfirmButton')); + + expect(saveIndexPattern).toHaveBeenCalled(); + await waitFor(() => { + expect(screen.queryByText('tim*')).not.toBeInTheDocument(); + }); + + expect(screen.getByText('na*')).toBeVisible(); + expect(indexPattern.sourceFilters).toEqual([{ value: 'na*', clientId: 2 }]); }); - test('should add a filter', async () => { + it('should add a filter', async () => { + const user = userEvent.setup(); const saveIndexPattern = jest.fn(async () => {}); - const component = shallow( - {}} - saveIndexPattern={saveIndexPattern} - /> - ); - - await component.instance().onAddFilter('na*'); - component.update(); // We are not calling `.setState` directly so we need to re-render + + const { indexPattern } = renderSourceFiltersTable({ + indexPattern: createDataView([{ value: 'tim*' }]), + saveIndexPattern, + }); + + await user.type(screen.getByTestId('fieldFilterInput'), 'na*'); + await user.click(screen.getByText('Add')); expect(saveIndexPattern).toBeCalled(); - expect(component).toMatchSnapshot(); + expect(await screen.findByText('na*')).toBeVisible(); + + expect(indexPattern.sourceFilters).toEqual([{ value: 'tim*' }, { value: 'na*' }]); }); - test('should update a filter', async () => { + it('should update a filter', async () => { + const user = userEvent.setup(); const saveIndexPattern = jest.fn(async () => {}); - const component = shallow( - {}} - saveIndexPattern={saveIndexPattern} - /> - ); - - await component.instance().saveFilter({ clientId: 'tim*', value: 'ti*' }); - component.update(); // We are not calling `.setState` directly so we need to re-render + + const { indexPattern } = renderSourceFiltersTable({ + indexPattern: createDataView([{ value: 'tim*' }]), + saveIndexPattern, + }); + + await user.click(screen.getByTestId('edit_filter-tim*')); + await user.clear(screen.getByTestId('filter_input_tim*')); + await user.type(screen.getByTestId('filter_input_tim*'), 'ti*'); + await user.click(screen.getByTestId('save_filter-tim*')); expect(saveIndexPattern).toBeCalled(); - expect(component).toMatchSnapshot(); + expect(await screen.findByText('ti*')).toBeVisible(); + expect(indexPattern.sourceFilters).toEqual([{ value: 'ti*', clientId: 1 }]); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap deleted file mode 100644 index 07a2e6ff0c270..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ /dev/null @@ -1,1455 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldEditor should render create new scripted field correctly 1`] = ` - - -

- -

-
- - - - - - - - - - } - label="Custom label" - > - - - - - - - - - - } - label={ - - } - > - - - - - - - } - fullWidth={true} - isInvalid={true} - label="Script" - > - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should render edit scripted field correctly 1`] = ` - - -

- -

-
- - - - - - - } - label="Custom label" - > - - - - - - - - - - } - label={ - - } - > - - - - - - - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should show conflict field warning 1`] = ` - - -

- -

-
- - - - - - - -   - - foobar - , - "mappingConflict": - - , - } - } - /> - - } - isInvalid={false} - label="Name" - > - - - - } - label="Custom label" - > - - - - - - - - - - } - label={ - - } - > - - - - - - - } - fullWidth={true} - isInvalid={true} - label="Script" - > - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should show deprecated lang warning 1`] = ` - - -

- -

-
- - - - - - - } - label="Custom label" - > - - - - -   - - - -   - - testlang - , - "painlessLink": - - , - } - } - /> - - } - label="Language" - > - - - - - - - } - label={ - - } - > - - - - - - - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should show multiple type field warning with a table containing indices 1`] = ` - - -

- -

-
- - - - - - - -   - - foobar - , - "mappingConflict": - - , - } - } - /> - - } - isInvalid={false} - label="Name" - > - - - - } - label="Custom label" - > - - - - - - - - -
- - - } - > - - - - - -
- - } - label={ - - } - > - - - - - - - } - fullWidth={true} - isInvalid={true} - label="Script" - > - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - -
- -
-`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.tsx.snap deleted file mode 100644 index 9d6a385cdfcff..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.tsx.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldFormatEditor should render normally 1`] = ` - - - - - - - } - > - - - -`; - -exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = ` - - - - - - - } - > - - - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx index ee6d5f5bac5d1..ab2f96d00604d 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx @@ -7,31 +7,28 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { FormatEditorServiceStart } from '@kbn/data-view-field-editor-plugin/public/service'; import type { FieldFormat } from '@kbn/field-formats-plugin/common'; -import { shallow } from 'enzyme'; -import React, { PureComponent } from 'react'; +import type { FormatEditorServiceStart } from '@kbn/data-view-field-editor-plugin/public/service'; import { FieldFormatEditor } from './field_format_editor'; +import { render, screen } from '@testing-library/react'; +import React, { PureComponent } from 'react'; class TestEditor extends PureComponent { - render() { - if (this.props) { - return null; - } - return
Test editor
; - } + render = () =>
Test editor
; } +const numberFormatEditorFactory = () => Promise.resolve(TestEditor); + const formatEditors: FormatEditorServiceStart['fieldFormatEditors'] = { - getById: jest.fn( - () => () => Promise.resolve(TestEditor) + getById: jest.fn((id: string) => + id === 'number' ? numberFormatEditorFactory : undefined ) as unknown as FormatEditorServiceStart['fieldFormatEditors']['getById'], - getAll: jest.fn(), + getAll: jest.fn(() => []), }; describe('FieldFormatEditor', () => { it('should render normally', async () => { - const component = shallow( + render( { /> ); - expect(component).toMatchSnapshot(); + expect(formatEditors.getById).toHaveBeenCalledWith('number'); + expect(await screen.findByTestId('test-editor')).toBeVisible(); }); it('should render nothing if there is no editor for the format', async () => { - const component = shallow( + render( { /> ); - expect(component).toMatchSnapshot(); + expect(formatEditors.getById).toHaveBeenCalledWith('ip'); + expect(await screen.queryByTestId('test-editor')).not.toBeInTheDocument(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.tsx.snap deleted file mode 100644 index 21f7be4f2c091..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.tsx.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptingDisabledCallOut should render normally 1`] = ` - - - } - > -

- -

-
- -
-`; - -exports[`ScriptingDisabledCallOut should render nothing if not visible 1`] = `""`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap deleted file mode 100644 index e654de9cd9ec1..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap +++ /dev/null @@ -1,75 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptingWarningCallOut should render normally 1`] = ` -Array [ -
-

- Familiarize yourself with - - and - - before using this feature. Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow and, if done incorrectly, can cause Kibana to become unusable. -

-
, -
, -
-

-

-
-
-
-

- For greater flexibility and Painless script support, use - - . -

-
-
-
, -
, -] -`; - -exports[`ScriptingWarningCallOut should render nothing if not visible 1`] = ` - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx index 57debfc3c9690..8229a1142ff17 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx @@ -8,20 +8,31 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - import { ScriptingDisabledCallOut } from './disabled_call_out'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; describe('ScriptingDisabledCallOut', () => { it('should render normally', async () => { - const component = shallow(); + renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(screen.getByText('Scripting disabled')).toBeVisible(); + expect( + screen.getByText( + 'All inline scripting has been disabled in Elasticsearch. You must enable inline scripting for at least one language in order to use scripted fields in Kibana.' + ) + ).toBeVisible(); }); it('should render nothing if not visible', async () => { - const component = shallow(); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toBeEmptyDOMElement(); + expect(screen.queryByText('Scripting disabled')).not.toBeInTheDocument(); + expect( + screen.queryByText( + 'All inline scripting has been disabled in Elasticsearch. You must enable inline scripting for at least one language in order to use scripted fields in Kibana.' + ) + ).not.toBeInTheDocument(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx index aa3ad6636d9d7..6028e4f604445 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx @@ -8,33 +8,47 @@ */ import React from 'react'; -import { mountWithI18nProvider } from '@kbn/test-jest-helpers'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public/context'; import { mockManagementPlugin } from '../../../../mocks'; +import { renderWithKibanaRenderContext } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; import { ScriptingWarningCallOut } from './warning_call_out'; describe('ScriptingWarningCallOut', () => { const mockedContext = mockManagementPlugin.createIndexPatternManagmentContext(); it('should render normally', async () => { - const component = mountWithI18nProvider(, { - wrappingComponent: KibanaContextProvider, - wrappingComponentProps: { - services: mockedContext, - }, - }); + renderWithKibanaRenderContext( + + + + ); - expect(component.render()).toMatchSnapshot(); + expect( + screen.getByText( + 'Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow and, if done incorrectly, can cause Kibana to become unusable.', + { exact: false } + ) + ).toBeVisible(); + expect(screen.getByText('Scripted fields are deprecated')).toBeVisible(); + expect( + screen.getByText('For greater flexibility and Painless script support, use', { exact: false }) + ).toBeVisible(); }); it('should render nothing if not visible', async () => { - const component = mountWithI18nProvider(, { - wrappingComponent: KibanaContextProvider, - wrappingComponentProps: { - services: mockedContext, - }, - }); + const { container } = renderWithKibanaRenderContext( + + + + ); - expect(component).toMatchSnapshot(); + expect(container).toBeEmptyDOMElement(); + expect(screen.queryByText('Scripted fields are deprecated')).not.toBeInTheDocument(); + expect( + screen.queryByText('For greater flexibility and Painless script support, use', { + exact: false, + }) + ).not.toBeInTheDocument(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap deleted file mode 100644 index e7264d2f15f2f..0000000000000 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap +++ /dev/null @@ -1,85 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptingHelpFlyout should render normally 1`] = ` - - - , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - } - } - tabs={ - Array [ - Object { - "content": , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - }, - Object { - "content": , - "data-test-subj": "testTab", - "id": "test", - "name": "Preview results", - }, - ] - } - /> - - -`; - -exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = ` - - - , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - } - } - tabs={ - Array [ - Object { - "content": , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - }, - Object { - "content": , - "data-test-subj": "testTab", - "id": "test", - "name": "Preview results", - }, - ] - } - /> - - -`; diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx index 6f8b03764e706..34194b8c93691 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/field_editor/components/scripting_help/help_flyout.test.tsx @@ -8,48 +8,51 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - +import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__/data_view'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; +import { screen, within } from '@testing-library/react'; import { ScriptingHelpFlyout } from './help_flyout'; -import type { DataView } from '@kbn/data-views-plugin/public'; - -import type { ExecuteScript } from '../../types'; - -jest.mock('./test_script', () => ({ - TestScript: () => { - return `
mockTestScript
`; - }, +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: () => ({ + services: { + docLinks: { + links: { + scriptedFields: { + luceneExpressions: '#', + painless: '#', + painlessApi: '#', + painlessSyntax: '#', + }, + }, + }, + }, + }), })); -const indexPatternMock = {} as DataView; +const renderFlyout = (isVisible: boolean) => + renderWithI18n( + + ); describe('ScriptingHelpFlyout', () => { - it('should render normally', async () => { - const component = shallow( - {}) as unknown as ExecuteScript} - onClose={() => {}} - /> - ); + it('should render normally', () => { + renderFlyout(true); - expect(component).toMatchSnapshot(); + expect(within(screen.getByTestId('syntaxTab')).getByText('Syntax')).toBeVisible(); + expect(within(screen.getByTestId('testTab')).getByText('Preview results')).toBeVisible(); + expect(screen.getByText('Painless')).toBeVisible(); }); - it('should render nothing if not visible', async () => { - const component = shallow( - {}) as unknown as ExecuteScript} - onClose={() => {}} - /> - ); + it('should render nothing if not visible', () => { + const { container } = renderFlyout(false); - expect(component).toMatchSnapshot(); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/src/platform/plugins/shared/data_view_management/public/components/field_editor/field_editor.test.tsx b/src/platform/plugins/shared/data_view_management/public/components/field_editor/field_editor.test.tsx index 0b2a0d7cef33e..ba08f22ae670c 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/field_editor/field_editor.test.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/field_editor/field_editor.test.tsx @@ -7,318 +7,318 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/public'; -import type { FieldFormatInstanceType } from '@kbn/field-formats-plugin/common'; -import { findTestSubject } from '@elastic/eui/lib/test'; - +import type { DataView, FieldSpec } from '@kbn/data-views-plugin/public'; import type { FieldEdiorProps } from './field_editor'; -import { FieldEditor } from './field_editor'; - -import { mockManagementPlugin } from '../../mocks'; -import { createComponentWithContext } from '../test_utils'; - -jest.mock('@elastic/eui', () => ({ - ...jest.requireActual('@elastic/eui'), - EuiBasicTable: 'eui-basic-table', - EuiButton: 'eui-button', - EuiButtonEmpty: 'eui-button-empty', - EuiCallOut: 'eui-call-out', - EuiCode: 'eui-code', - EuiConfirmModal: 'eui-confirm-modal', - EuiFieldNumber: 'eui-field-number', - EuiFieldText: 'eui-field-text', - EuiFlexGroup: 'eui-flex-group', - EuiFlexItem: 'eui-flex-item', - EuiForm: 'eui-form', - EuiFormRow: 'eui-form-row', - EuiIcon: 'eui-icon', - EuiLink: 'eui-link', - EuiOverlayMask: 'eui-overlay-mask', - EuiSelect: 'eui-select', - EuiSpacer: 'eui-spacer', - EuiText: 'eui-text', - EuiTextArea: 'eui-textArea', - htmlIdGenerator: () => () => 42, - euiPaletteColorBlind: () => ['red'], +import { createStubDataView } from '@kbn/data-views-plugin/public/data_views/data_view.stub'; +import React from 'react'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public/context'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +const monacoModuleName = '@kbn/monaco'; + +jest.doMock('@kbn/code-editor', () => ({ + CodeEditor: ({ + height: _height, + languageId: _languageId, + onChange, + value, + width: _width, + ...props + }: { + height: string; + languageId: string; + onChange: (value: string) => void; + value: string; + width: string; + }) => ( +