diff --git a/src/shared/constants/qa/components.ts b/src/shared/constants/qa/components.ts index d3d3b89b49..fc0ff5f6f6 100644 --- a/src/shared/constants/qa/components.ts +++ b/src/shared/constants/qa/components.ts @@ -145,8 +145,17 @@ export enum DialogDashWidgetQA { export const enum DialogFieldEditorQA { ApplyButton = 'dialog-field-editor-apply-button', CancelButton = 'dialog-field-editor-cancel-button', + EditNameButton = 'dialog-field-editor-edit-name-button', } +export const DialogFilterQA = { + Dialog: 'dialog-filter', + ApplyButton: 'dialog-filter-apply-button', + CancelButton: 'dialog-filter-cancel-button', + ListItem: 'dialog-filter-list-item', + OperationSelect: 'operation-select', +} as const; + export const enum NavigationMinimalPlaceSelectQa { Connections = 'navigation-minimal-place-connections', Datasets = 'navigation-minimal-place-datasets', diff --git a/src/shared/constants/qa/datasets.ts b/src/shared/constants/qa/datasets.ts index 54d031a92b..c13257122c 100644 --- a/src/shared/constants/qa/datasets.ts +++ b/src/shared/constants/qa/datasets.ts @@ -9,12 +9,57 @@ export enum ParametersQA { ParametersTabSection = 'parameters-tab-section', } +export const FiltersQA = { + FiltersTabSection: 'filters-tab-section', + TableDeleteRowBtn: 'filters-table-delete-row-button', +} as const; + +export const RelationsMapQA = { + RelationsMap: 'ds-relations-map', +} as const; + export enum DatasetFieldsTabQa { + DatasetEditor = 'ds-editor', TableRow = 'dataset-fields-table-row', FieldNameColumnInput = 'dataset-fields-name-column-input', + FieldIndexColumnCheckbox = 'dataset-fields-index-column-checkbox', + FieldIndexHeaderColumnCheckbox = 'dataset-fields-index-header-column-checkbox', + FieldIdColumnInput = 'dataset-fields-id-column-input', + FieldDescriptionColumnInput = 'dataset-fields-description-column-input', + FieldSourceColumnBtn = 'dataset-fields-source-column-btn', FieldSettingsColumnIcon = 'dataset-fields-settings-column-icon', + FieldVisibleColumnIcon = 'dataset-fields-visible-column-icon', + FieldTypeColumnBtn = 'dataset-fields-type-column-btn', + FieldAggregationColumnBtn = 'dataset-fields-aggregation-column-btn', + FieldTypeColumnItem = 'dataset-fields-type-column-item', + FieldContextMenuBtn = 'dataset-fields-context-menu-btn', + FieldContextMenuPopup = 'dataset-fields-context-menu-popup', + TableSettingsBtn = 'dataset-editor-table-settings-btn', + BatchActionsPanel = 'dataset-editor-actions-panel', } +export const DatasetFieldTabBatchPanelQa = { + BatchDelete: 'ds-fields-batch-delete', + BatchHide: 'ds-fields-batch-hide', + BatchShow: 'ds-fields-batch-show', + BatchType: 'ds-fields-batch-type', + BatchAggregation: 'ds-fields-batch-aggregation', +} as const; + +export const DatasetFieldContextMenuItemsQA = { + DUPLICATE: 'ds-field-context-menu-duplicate', + EDIT: 'ds-field-context-menu-edit', + RLS: 'ds-field-context-menu-rls', + COPY_GUID: 'ds-field-context-menu-copy-guid', + REMOVE: 'ds-field-context-menu-remove', + INSPECT: 'ds-field-context-menu-inspect', +} as const; + +export const DatasetEditorTableSettingsItems = { + ShowHidden: 'ds-table-settings-menu-show-hidden', + ShowId: 'ds-table-settings-menu-show-id', +} as const; + export enum DatasetFieldSettingsDialogQa { Dialog = 'dataset-field-settings-dialog', ColorSettingsButton = 'dataset-field-settings-dialog-color-settings-btn', @@ -58,6 +103,8 @@ export enum DatasetSourcesLeftPanelQA { export enum DatasetActionQA { CreateButton = 'dataset-create-button', + SettingsButton = 'dataset-settings-button', + SettingsShowPreviewByDefault = 'dataset-settings-show-preview-item', } export const enum DatasetDialogRelationQA { @@ -79,8 +126,20 @@ export const DatasetSourcesTableQa = { SourceContextMenuBtn: 'ds-source-context-menu-btn', SourceContextMenuDelete: 'ds-source-context-menu-delete', SourceContextMenuModify: 'ds-source-context-menu-modify', + SourcesAddItemBtn: 'ds-source-add-item-button', } as const; export enum DatasetPreviewQA { Preview = 'dataset-preview', + RowCountInput = 'ds-preview-header-row-count-input', + ClosePreviewBtn = 'ds-preview-header-close-btn', } + +export const DatasetSourceEditorDialogQA = { + Dialog: 'source-editor-dialog', + EditTitleInput: 'source-editor-title', + EditPathInput: 'source-editor-path', + SourceEditorSwitch: 'datasets-source-switcher', + ApplyBtn: 'source-editor-apply', + CancelBtn: 'source-editor-cancel', +} as const; diff --git a/src/shared/constants/qa/edit-history.ts b/src/shared/constants/qa/edit-history.ts new file mode 100644 index 0000000000..3e5c88fe79 --- /dev/null +++ b/src/shared/constants/qa/edit-history.ts @@ -0,0 +1,4 @@ +export const EditHistoryQA = { + UndoBtn: 'edit-history-undo-btn', + RedoBtn: 'edit-history-redo-btn', +}; diff --git a/src/shared/constants/qa/index.ts b/src/shared/constants/qa/index.ts index d66afc992b..0087db624c 100644 --- a/src/shared/constants/qa/index.ts +++ b/src/shared/constants/qa/index.ts @@ -15,3 +15,4 @@ export * from './uikit'; export * from './wizard'; export * from './workbooks'; export * from './shared-entries'; +export * from './edit-history'; diff --git a/src/ui/components/DatasetFieldList/DatasetFieldList.tsx b/src/ui/components/DatasetFieldList/DatasetFieldList.tsx index ed055a8ba3..d432d6b2d8 100644 --- a/src/ui/components/DatasetFieldList/DatasetFieldList.tsx +++ b/src/ui/components/DatasetFieldList/DatasetFieldList.tsx @@ -6,7 +6,7 @@ import block from 'bem-cn-lite'; import {I18n} from '../../../i18n'; import type {DatasetField} from '../../../shared'; -import {DatasetFieldType, isParameter} from '../../../shared'; +import {DatasetFieldType, DialogFilterQA, isParameter} from '../../../shared'; import DataTypeIcon from '../DataTypeIcon/DataTypeIcon'; import './DatasetFieldList.scss'; @@ -30,7 +30,7 @@ const DatasetFieldList: React.FC = (props) => { const fieldType = isParameter(field) ? DatasetFieldType.Parameter : type; return ( -
+
} this.onApply(); }} + qa={DialogFilterQA.Dialog} >
{this.renderHeader()} @@ -321,7 +323,7 @@ class DialogFilter extends React.Component value={value} options={options} onUpdate={this.onChangeOperation} - qa="operation-select" + qa={DialogFilterQA.OperationSelect} />
@@ -404,8 +406,12 @@ class DialogFilter extends React.Component onClickButtonApply={onClickButtonApply} textButtonApply={textButtonApply} textButtonCancel={i18n('button_cancel')} + propsButtonCancel={{ + qa: DialogFilterQA.CancelButton, + }} propsButtonApply={{ disabled: this.isApplyButtonDisabled(), + qa: DialogFilterQA.ApplyButton, }} > {useSuggest && dimensions.length === VALUES_LOAD_LIMIT && ( diff --git a/src/ui/components/FieldEditor/components/NameHeader.tsx b/src/ui/components/FieldEditor/components/NameHeader.tsx index a3b78034d2..1ed037cf52 100644 --- a/src/ui/components/FieldEditor/components/NameHeader.tsx +++ b/src/ui/components/FieldEditor/components/NameHeader.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {Pencil} from '@gravity-ui/icons'; import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; +import {DialogFieldEditorQA} from 'shared'; const b = block('dl-field-editor'); @@ -17,7 +18,12 @@ export const NameHeader = ({title, onStartEdit}: NameFieldProps) => { {title} - diff --git a/src/ui/hooks/useEditHistoryActions.ts b/src/ui/hooks/useEditHistoryActions.ts index 856a7f957b..f59523c658 100644 --- a/src/ui/hooks/useEditHistoryActions.ts +++ b/src/ui/hooks/useEditHistoryActions.ts @@ -3,6 +3,7 @@ import React from 'react'; import {ArrowUturnCcwLeft, ArrowUturnCwRight} from '@gravity-ui/icons'; import {I18n} from 'i18n'; import {useDispatch, useSelector} from 'react-redux'; +import {EditHistoryQA} from 'shared'; import type {AdditionalButtonTemplate} from 'ui/components/ActionPanel/components/ChartSaveControls/types'; import {useAdditionalItems} from 'ui/components/ActionPanel/components/ChartSaveControls/useAdditionalItems'; import {REDO_HOTKEY, UNDO_HOTKEY} from 'ui/constants/misc'; @@ -70,6 +71,7 @@ export function useEditHistoryActions(options: UseEditHistoryActionsOptions) { disabled: !canGoBack, title: i18n('button_undo'), hotkey: UNDO_HOTKEY.join('+'), + qa: EditHistoryQA.UndoBtn, }, { key: 'redo', @@ -79,6 +81,7 @@ export function useEditHistoryActions(options: UseEditHistoryActionsOptions) { disabled: !canGoForward, title: i18n('button_redo'), hotkey: REDO_HOTKEY.join('+'), + qa: EditHistoryQA.RedoBtn, }, ]; }, [canGoBack, canGoForward, handleGoBack, handleGoForward, iconSize]); diff --git a/src/ui/units/datasets/components/DatasetTable/columns/Description.tsx b/src/ui/units/datasets/components/DatasetTable/columns/Description.tsx index 7e202fe595..3733cb7ec1 100644 --- a/src/ui/units/datasets/components/DatasetTable/columns/Description.tsx +++ b/src/ui/units/datasets/components/DatasetTable/columns/Description.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type {Column} from '@gravity-ui/react-data-table'; import block from 'bem-cn-lite'; import {I18n} from 'i18n'; -import type {DatasetField} from 'shared'; +import {type DatasetField, DatasetFieldsTabQa} from 'shared'; import {TableTextInput} from '../components'; import type {ColumnItem} from '../types'; @@ -41,6 +41,7 @@ export const getDescriptionColumn = (args: GetDescriptionColumnArgs) => { index={index} setActiveRow={setActiveRow} onUpdate={getUpdateHandler(row)} + qa={DatasetFieldsTabQa.FieldDescriptionColumnInput} disabled={readonly} /> ); diff --git a/src/ui/units/datasets/components/DatasetTable/columns/Hidden.tsx b/src/ui/units/datasets/components/DatasetTable/columns/Hidden.tsx index 091842b990..50e5e0dacc 100644 --- a/src/ui/units/datasets/components/DatasetTable/columns/Hidden.tsx +++ b/src/ui/units/datasets/components/DatasetTable/columns/Hidden.tsx @@ -7,6 +7,7 @@ import {Button, Icon} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {I18n} from 'i18n'; import type {DatasetField} from 'shared'; +import {DatasetFieldsTabQa} from 'shared'; import {isHiddenSupported} from '../utils'; @@ -39,6 +40,7 @@ export const getHiddenColumn = ({ title={value ? i18n('button_display-field') : i18n('button_hide-field')} disabled={unsupported || readonly} onClick={() => onUpdate(row)} + qa={DatasetFieldsTabQa.FieldVisibleColumnIcon} > { setActiveRow={setActiveRow} onUpdate={getUpdateHandler(row)} disabled={readonly} + qa={DatasetFieldsTabQa.FieldIdColumnInput} /> ); }, diff --git a/src/ui/units/datasets/components/DatasetTable/columns/IndexColumn.tsx b/src/ui/units/datasets/components/DatasetTable/columns/IndexColumn.tsx index 5e1c82a144..44e4d479e3 100644 --- a/src/ui/units/datasets/components/DatasetTable/columns/IndexColumn.tsx +++ b/src/ui/units/datasets/components/DatasetTable/columns/IndexColumn.tsx @@ -5,6 +5,7 @@ import DataTable from '@gravity-ui/react-data-table'; import {Checkbox} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import type {DatasetField, DatasetSelectionMap} from 'shared'; +import {DatasetFieldsTabQa} from 'shared'; const b = block('dataset-table'); @@ -40,6 +41,7 @@ export const getIndexColumn = ({ indeterminate={indeterminate} onUpdate={onSelectAllChange} disabled={readonly} + qa={DatasetFieldsTabQa.FieldIndexHeaderColumnCheckbox} /> ), render: function IndexColumnItem({index, row}) { @@ -66,6 +68,7 @@ export const getIndexColumn = ({ size={'l'} disabled={readonly} onChange={handleCheckboxChange} + qa={DatasetFieldsTabQa.FieldIndexColumnCheckbox} />
{index + 1}
diff --git a/src/ui/units/datasets/components/DatasetTable/columns/Source.tsx b/src/ui/units/datasets/components/DatasetTable/columns/Source.tsx index 134b7b877a..e66713f671 100644 --- a/src/ui/units/datasets/components/DatasetTable/columns/Source.tsx +++ b/src/ui/units/datasets/components/DatasetTable/columns/Source.tsx @@ -6,7 +6,7 @@ import DataTable from '@gravity-ui/react-data-table'; import {Button, Icon} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {I18n} from 'i18n'; -import type {DatasetField, DatasetSourceAvatar} from 'shared'; +import {type DatasetField, DatasetFieldsTabQa, type DatasetSourceAvatar} from 'shared'; import {FORMULA_CALC_MODE} from '../constants'; import {getFieldSourceTitle, sortSourceColumn} from '../utils'; @@ -48,6 +48,7 @@ export const getSourceColumn = (args: GetSourceColumnArgs): Column width="max" disabled={readonly} onClick={() => openDialogFieldEditor(row)} + qa={DatasetFieldsTabQa.FieldSourceColumnBtn} > {isFormulaField ? ( diff --git a/src/ui/units/datasets/components/DatasetTable/components/AggregationSelect/AggregationSelect.tsx b/src/ui/units/datasets/components/DatasetTable/components/AggregationSelect/AggregationSelect.tsx index 70f0e4fcb0..2d1ffb4dfb 100644 --- a/src/ui/units/datasets/components/DatasetTable/components/AggregationSelect/AggregationSelect.tsx +++ b/src/ui/units/datasets/components/DatasetTable/components/AggregationSelect/AggregationSelect.tsx @@ -5,6 +5,7 @@ import type {SelectOption, SelectRenderControlProps} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {connect} from 'react-redux'; import type {DatasetField, DatasetFieldAggregation} from 'shared'; +import {DatasetFieldsTabQa} from 'shared'; import type {DatalensGlobalState} from 'ui'; import {getDatasetLabelValue, getSelectedValueForSelect} from '../../../../../../utils/helpers'; @@ -137,6 +138,7 @@ class AggregationSelectComponent extends React.Component { ref={ref as React.Ref} view="flat" className={b('select-control', {[this.type]: true})} + qa={DatasetFieldsTabQa.FieldAggregationColumnBtn} > {getDatasetLabelValue(aggregationTitle)} diff --git a/src/ui/units/datasets/components/DatasetTable/components/BatchActionPanel/BatchActionPanel.tsx b/src/ui/units/datasets/components/DatasetTable/components/BatchActionPanel/BatchActionPanel.tsx index 1d6733bed5..c3a1ac8c0b 100644 --- a/src/ui/units/datasets/components/DatasetTable/components/BatchActionPanel/BatchActionPanel.tsx +++ b/src/ui/units/datasets/components/DatasetTable/components/BatchActionPanel/BatchActionPanel.tsx @@ -4,6 +4,7 @@ import {Eye, EyeSlash, Pencil, TrashBin} from '@gravity-ui/icons'; import type {ActionsPanelProps} from '@gravity-ui/uikit'; import {ActionsPanel, Icon} from '@gravity-ui/uikit'; import {I18n} from 'i18n'; +import {DatasetFieldTabBatchPanelQa, DatasetFieldsTabQa} from 'shared'; import {BatchFieldAction} from '../../constants'; @@ -34,6 +35,7 @@ export const BatchActionPanel = ({ , i18n('button_batch-remove'), ], + qa: DatasetFieldTabBatchPanelQa.BatchDelete, onClick: () => onAction(BatchFieldAction.Remove), }, }, @@ -55,6 +57,7 @@ export const BatchActionPanel = ({ , i18n('button_batch-hide'), ], + qa: DatasetFieldTabBatchPanelQa.BatchHide, onClick: () => onAction(BatchFieldAction.Hide), }, }, @@ -76,6 +79,7 @@ export const BatchActionPanel = ({ , i18n('button_batch-show'), ], + qa: DatasetFieldTabBatchPanelQa.BatchShow, onClick: () => onAction(BatchFieldAction.Show), }, }, @@ -97,6 +101,7 @@ export const BatchActionPanel = ({ , i18n('button_batch-type'), ], + qa: DatasetFieldTabBatchPanelQa.BatchType, onClick: () => onAction(BatchFieldAction.Type), }, }, @@ -118,6 +123,7 @@ export const BatchActionPanel = ({ , i18n('button_batch-aggregation'), ], + qa: DatasetFieldTabBatchPanelQa.BatchAggregation, onClick: () => onAction(BatchFieldAction.Aggregation), }, }, @@ -139,6 +145,7 @@ export const BatchActionPanel = ({ className={className} onClose={onClose} renderNote={() => i18n('label_batch-selected-fields-count', {value: count})} + qa={DatasetFieldsTabQa.BatchActionsPanel} /> ); }; diff --git a/src/ui/units/datasets/components/DatasetTable/components/DisplaySettings.tsx b/src/ui/units/datasets/components/DatasetTable/components/DisplaySettings.tsx index 34f11bc276..d232d73350 100644 --- a/src/ui/units/datasets/components/DatasetTable/components/DisplaySettings.tsx +++ b/src/ui/units/datasets/components/DatasetTable/components/DisplaySettings.tsx @@ -4,6 +4,7 @@ import {Gear} from '@gravity-ui/icons'; import {Button, Icon, Select, type SelectProps} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {I18n} from 'i18n'; +import {DatasetEditorTableSettingsItems, DatasetFieldsTabQa} from 'shared'; import type {EditorItemToDisplay} from '../../../store/types'; @@ -16,7 +17,13 @@ const renderControl: SelectProps['renderControl'] = (args) => { const {triggerProps, ref} = args; return ( - ); @@ -31,10 +38,15 @@ export const DisplaySettings = ({value, onUpdate}: Pick - + {i18n('label_display-hidden-fields')} - {i18n('label_display-fields-id')} + + {i18n('label_display-fields-id')} + ); }; diff --git a/src/ui/units/datasets/components/DatasetTable/components/FieldActionsPopup.tsx b/src/ui/units/datasets/components/DatasetTable/components/FieldActionsPopup.tsx index 0baaa165db..e9e81a8b74 100644 --- a/src/ui/units/datasets/components/DatasetTable/components/FieldActionsPopup.tsx +++ b/src/ui/units/datasets/components/DatasetTable/components/FieldActionsPopup.tsx @@ -4,6 +4,7 @@ import type {DropdownMenuItem, DropdownMenuItemMixed} from '@gravity-ui/uikit'; import {CopyToClipboard, DropdownMenu} from '@gravity-ui/uikit'; import {i18n} from 'i18n'; import type {DatasetField} from 'shared'; +import {DatasetFieldsTabQa} from 'shared'; import {FieldAction, GROUPED_ITEMS, READONLY_AVAILABLE_ITEMS} from '../constants'; import type {MenuItem} from '../types'; @@ -46,7 +47,7 @@ const getMenuItems = ( ): DropdownMenuItemMixed[] => { return GROUPED_ITEMS.reduce[]>((items, group) => { const groupWithoutHidden = group.reduce[]>( - (group, {action, label, theme, hidden = false}) => { + (group, {action, label, theme, hidden = false, qa}) => { if (readonly && !READONLY_AVAILABLE_ITEMS.includes(action)) { return group; } @@ -57,6 +58,7 @@ const getMenuItems = ( hidden, text: , action: () => onClick({action, field}), + qa, }); } @@ -88,8 +90,12 @@ export function FieldActionsPopup(props: FieldActionsPopupProps) { items={getMenuItems(field, onItemClick, readonly)} popupProps={{ placement: ['bottom-end', 'top-end'], + qa: DatasetFieldsTabQa.FieldContextMenuPopup, }} onOpenToggle={handleMenuToggle} + defaultSwitcherProps={{ + qa: DatasetFieldsTabQa.FieldContextMenuBtn, + }} /> ); } diff --git a/src/ui/units/datasets/components/DatasetTable/components/TypeSelect/TypeSelect.tsx b/src/ui/units/datasets/components/DatasetTable/components/TypeSelect/TypeSelect.tsx index 4c81fe2602..f644320099 100644 --- a/src/ui/units/datasets/components/DatasetTable/components/TypeSelect/TypeSelect.tsx +++ b/src/ui/units/datasets/components/DatasetTable/components/TypeSelect/TypeSelect.tsx @@ -5,6 +5,7 @@ import type {SelectOption, SelectRenderControlProps} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {connect} from 'react-redux'; import type {DATASET_FIELD_TYPES, DatasetField} from 'shared'; +import {DatasetFieldsTabQa} from 'shared'; import type {DatalensGlobalState} from 'ui'; import {DataTypeIcon} from 'ui'; @@ -54,6 +55,7 @@ class TypeSelectComponent extends React.Component { value: type, content: getDatasetLabelValue(type), disabled: validation.isLoading, + qa: `${DatasetFieldsTabQa.FieldTypeColumnItem}-${type}`, }; }) .sort((current, next) => { @@ -98,6 +100,7 @@ class TypeSelectComponent extends React.Component { ref={ref as React.Ref} view="flat" className={b('select-control')} + qa={DatasetFieldsTabQa.FieldTypeColumnBtn} > {this.renderSelectOption({value, content: getDatasetLabelValue(value)})} diff --git a/src/ui/units/datasets/components/DatasetTable/constants.ts b/src/ui/units/datasets/components/DatasetTable/constants.ts index 95dfa0bf36..c63803890c 100644 --- a/src/ui/units/datasets/components/DatasetTable/constants.ts +++ b/src/ui/units/datasets/components/DatasetTable/constants.ts @@ -1,5 +1,5 @@ import type {DatasetFieldCalcMode} from 'shared'; -import {Feature} from 'shared'; +import {DatasetFieldContextMenuItemsQA, Feature} from 'shared'; import {Utils} from 'ui'; import {isEnabledFeature} from 'ui/utils/isEnabledFeature'; @@ -24,10 +24,26 @@ export enum BatchFieldAction { Aggregation = 'aggregation', } -const DUPLICATE: MenuItem = {action: FieldAction.Duplicate, label: 'button_duplicate'}; -const EDIT: MenuItem = {action: FieldAction.Edit, label: 'button_edit'}; -const RLS: MenuItem = {action: FieldAction.Rls, label: 'button_row-level-security'}; -const COPY_GUID: MenuItem = {action: FieldAction.CopyGuid, label: 'button_copy-id'}; +const DUPLICATE: MenuItem = { + action: FieldAction.Duplicate, + label: 'button_duplicate', + qa: DatasetFieldContextMenuItemsQA.DUPLICATE, +}; +const EDIT: MenuItem = { + action: FieldAction.Edit, + label: 'button_edit', + qa: DatasetFieldContextMenuItemsQA.EDIT, +}; +const RLS: MenuItem = { + action: FieldAction.Rls, + label: 'button_row-level-security', + qa: DatasetFieldContextMenuItemsQA.RLS, +}; +const COPY_GUID: MenuItem = { + action: FieldAction.CopyGuid, + label: 'button_copy-id', + qa: DatasetFieldContextMenuItemsQA.COPY_GUID, +}; export const getCommonMenuItemsData = () => { if (isEnabledFeature(Feature.DatasetsRLS)) { @@ -39,8 +55,22 @@ export const getCommonMenuItemsData = () => { export const GROUPED_ITEMS: MenuItem[][] = [ getCommonMenuItemsData(), - [{action: FieldAction.Remove, label: 'button_remove', theme: 'danger'}], - [{action: FieldAction.Inspect, label: 'button_inspect', hidden: !Utils.isSuperUser()}], + [ + { + action: FieldAction.Remove, + label: 'button_remove', + theme: 'danger', + qa: DatasetFieldContextMenuItemsQA.REMOVE, + }, + ], + [ + { + action: FieldAction.Inspect, + label: 'button_inspect', + hidden: !Utils.isSuperUser(), + qa: DatasetFieldContextMenuItemsQA.INSPECT, + }, + ], ]; export const READONLY_AVAILABLE_ITEMS = [FieldAction.CopyGuid]; diff --git a/src/ui/units/datasets/components/DatasetTable/types.ts b/src/ui/units/datasets/components/DatasetTable/types.ts index dd693b607e..2b4705e0ac 100644 --- a/src/ui/units/datasets/components/DatasetTable/types.ts +++ b/src/ui/units/datasets/components/DatasetTable/types.ts @@ -13,6 +13,7 @@ export type MenuItem = { label: string; theme?: MenuItemProps['theme']; hidden?: boolean; + qa?: string; }; export type UpdatePayload = { diff --git a/src/ui/units/datasets/components/FilterSection/FilterSection.tsx b/src/ui/units/datasets/components/FilterSection/FilterSection.tsx index dd3280dfec..72965fb84c 100644 --- a/src/ui/units/datasets/components/FilterSection/FilterSection.tsx +++ b/src/ui/units/datasets/components/FilterSection/FilterSection.tsx @@ -4,6 +4,7 @@ import {Alert, spacing} from '@gravity-ui/uikit'; import {I18n} from 'i18n'; import {useHistory, useLocation} from 'react-router'; import type {DatasetField, DatasetOptions} from 'shared'; +import {FiltersQA} from 'shared'; import type {ApplyData} from 'ui'; import type {ObligatoryFilter} from '../../typings/dataset'; @@ -90,6 +91,7 @@ const FilterSection: React.FC = (props: FilterSectionProps) isListUpdating={progress} controlSettings={controlSettings} checkIsRowValid={checkIsRowValid} + qa={FiltersQA.FiltersTabSection} /> ); }; diff --git a/src/ui/units/datasets/components/FilterSection/useFilterSection.ts b/src/ui/units/datasets/components/FilterSection/useFilterSection.ts index f423ec32fe..7a3a6f83b6 100644 --- a/src/ui/units/datasets/components/FilterSection/useFilterSection.ts +++ b/src/ui/units/datasets/components/FilterSection/useFilterSection.ts @@ -3,6 +3,7 @@ import React from 'react'; import without from 'lodash/without'; import {useDispatch, useSelector} from 'react-redux'; import type {DatasetField, DatasetOptions} from 'shared'; +import {FiltersQA} from 'shared'; import type {ApplyData} from 'ui'; import {openDialogFilter} from '../../../../store/actions/dialog'; @@ -106,6 +107,7 @@ export const useFilterSection = (args: UseFilterSectionArgs): UseFilterSection = deleteFilter(filter.id); }, readonly, + qa: FiltersQA.TableDeleteRowBtn, }; }, [deleteFilter, filters, readonly]); diff --git a/src/ui/units/datasets/components/PreviewHeader/PreviewHeader.tsx b/src/ui/units/datasets/components/PreviewHeader/PreviewHeader.tsx index 379903f1eb..67979d73ad 100644 --- a/src/ui/units/datasets/components/PreviewHeader/PreviewHeader.tsx +++ b/src/ui/units/datasets/components/PreviewHeader/PreviewHeader.tsx @@ -5,6 +5,7 @@ import {Button, Icon, TextInput} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {i18n} from 'i18n'; import _debounce from 'lodash/debounce'; +import {DatasetPreviewQA} from 'shared'; import {formatNumber} from 'shared/modules/format-units/formatUnit'; import {VIEW_PREVIEW} from '../../constants'; @@ -46,6 +47,7 @@ class PreviewHeader extends React.Component { type="number" value={String(amountPreviewRows)} onUpdate={this.changeAmountPreviewRows} + qa={DatasetPreviewQA.RowCountInput} /> {i18n('dataset.dataset-editor.modify', 'label_max-amount-rows', { @@ -97,6 +99,7 @@ class PreviewHeader extends React.Component { view="flat" title={i18n('dataset.dataset-editor.modify', 'button_preview-close')} onClick={this.closePreview} + qa={DatasetPreviewQA.ClosePreviewBtn} > diff --git a/src/ui/units/datasets/components/RelationsMap/RelationsMap.js b/src/ui/units/datasets/components/RelationsMap/RelationsMap.js index 67c9e28f3a..d0488f4376 100644 --- a/src/ui/units/datasets/components/RelationsMap/RelationsMap.js +++ b/src/ui/units/datasets/components/RelationsMap/RelationsMap.js @@ -9,7 +9,7 @@ import _isEqual from 'lodash/isEqual'; import _noop from 'lodash/noop'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; -import {AvatarQA} from 'shared'; +import {AvatarQA, RelationsMapQA} from 'shared'; import {PlaceholderIllustration} from 'ui/components/PlaceholderIllustration/PlaceholderIllustration'; import {JOIN_TYPES_ICONS, SVG_NAMESPACE_URI} from '../../constants'; @@ -355,7 +355,7 @@ class RelationsMap extends React.Component { className={b({ over: isOver && !isDisabledDropSource, })} - data-qa="ds-relations-map" + data-qa={RelationsMapQA.RelationsMap} >
{ const AddSourceButton: React.FC = ({onClick, disabled}) => (
- diff --git a/src/ui/units/datasets/components/SourceEditorDialog/SourceEditorDialog.tsx b/src/ui/units/datasets/components/SourceEditorDialog/SourceEditorDialog.tsx index 44b37e45ff..e4da52187b 100644 --- a/src/ui/units/datasets/components/SourceEditorDialog/SourceEditorDialog.tsx +++ b/src/ui/units/datasets/components/SourceEditorDialog/SourceEditorDialog.tsx @@ -8,6 +8,7 @@ import type {DatalensGlobalState} from 'ui'; import {I18n} from '../../../../../i18n'; import type {DatasetComponentError} from '../../../../../shared'; +import {DatasetSourceEditorDialogQA} from '../../../../../shared'; import DialogConfirm from '../../../../components/DialogConfirm/DialogConfirm'; import type {FormValidationError} from '../../helpers/validation'; import {VALIDATION_ERROR} from '../../helpers/validation'; @@ -211,7 +212,7 @@ const SourceEditorDialog: React.FC = (props) => { return ( = (props) => { )} = (props) => { loading={loading} textButtonCancel={i18n('button_cancel')} textButtonApply={isNewSource ? i18n('button_create') : i18n('button_apply')} - propsButtonApply={{disabled: !sourceChanged}} + propsButtonApply={{ + disabled: !sourceChanged, + qa: DatasetSourceEditorDialogQA.ApplyBtn, + }} + propsButtonCancel={{qa: DatasetSourceEditorDialogQA.CancelBtn}} onClickButtonCancel={attemptToCloseDialog} onClickButtonApply={_onApply} /> diff --git a/src/ui/units/datasets/components/SourceEditorDialog/components/Form.tsx b/src/ui/units/datasets/components/SourceEditorDialog/components/Form.tsx index 5092917835..ca522c7b6f 100644 --- a/src/ui/units/datasets/components/SourceEditorDialog/components/Form.tsx +++ b/src/ui/units/datasets/components/SourceEditorDialog/components/Form.tsx @@ -1,7 +1,7 @@ import React from 'react'; import block from 'bem-cn-lite'; -import _get from 'lodash/get'; +import {DatasetSourceEditorDialogQA} from 'shared'; import type {FormValidationError} from '../../../helpers/validation'; import type {FreeformSource} from '../../../store/types'; @@ -60,7 +60,7 @@ export const Form = ({ case 'textarea': { return ( = (props) => { return ( diff --git a/src/ui/units/datasets/containers/Dataset/ActionPanelRightItems.tsx b/src/ui/units/datasets/containers/Dataset/ActionPanelRightItems.tsx index 2c303257f8..9ad15abc53 100644 --- a/src/ui/units/datasets/containers/Dataset/ActionPanelRightItems.tsx +++ b/src/ui/units/datasets/containers/Dataset/ActionPanelRightItems.tsx @@ -132,7 +132,12 @@ export function ActionPanelRightItems(props: Props) { const {ref, triggerProps} = args; return ( - ); @@ -151,6 +156,7 @@ export function ActionPanelRightItems(props: Props) { key={ITEM_SHOW_PREVIEW_BY_DEFAULT} value={ITEM_SHOW_PREVIEW_BY_DEFAULT} disabled={isLoadingDataset || isValidationLoading} + qa={DatasetActionQA.SettingsShowPreviewByDefault} > {i18n('label_load_preview_by_default')} , diff --git a/src/ui/units/datasets/containers/DatasetEditor/DatasetEditor.js b/src/ui/units/datasets/containers/DatasetEditor/DatasetEditor.js index c8cd06ee03..fdca690eb7 100644 --- a/src/ui/units/datasets/containers/DatasetEditor/DatasetEditor.js +++ b/src/ui/units/datasets/containers/DatasetEditor/DatasetEditor.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import {compose} from 'recompose'; import {createStructuredSelector} from 'reselect'; -import {Feature} from 'shared'; +import {DatasetFieldsTabQa, Feature} from 'shared'; import {registry} from 'ui/registry'; import {selectDebugMode} from 'ui/store/selectors/user'; import {isEnabledFeature} from 'ui/utils/isEnabledFeature'; @@ -356,7 +356,7 @@ export class DatasetEditor extends React.Component { const {renderRLSDialog} = registry.datasets.functions.getAll(); return ( -
+
{ + const url = `datasets${DatasetsEntities.Basic.url}`; + + datalensTest('Invisible row should be hidden', async ({page}) => { + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + const rowsCount = await page.locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)).count(); + const hiddenBtn = page.locator(slct(DatasetFieldsTabQa.FieldVisibleColumnIcon)).first(); + await hiddenBtn.click(); + const settingsBtn = page.locator(slct(DatasetFieldsTabQa.TableSettingsBtn)); + await settingsBtn.click(); + const showHiddenMenuItem = await page.waitForSelector( + slct(DatasetEditorTableSettingsItems.ShowHidden), + ); + await showHiddenMenuItem.click(); + const rowsCountAfterHide = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCount).toBeGreaterThan(rowsCountAfterHide); + }); + + datalensTest('Id should be visible if enabled in settings', async ({page}) => { + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + const idColumnRowCount = await page + .locator(slct(DatasetFieldsTabQa.FieldIdColumnInput)) + .count(); + expect(idColumnRowCount).toBe(0); + const settingsBtn = page.locator(slct(DatasetFieldsTabQa.TableSettingsBtn)); + await settingsBtn.click(); + const showIdMenuItem = await page.waitForSelector( + slct(DatasetEditorTableSettingsItems.ShowId), + ); + await showIdMenuItem.click(); + const rowsCountAfterEnable = await page + .locator(slct(DatasetFieldsTabQa.FieldIdColumnInput)) + .count(); + expect(rowsCountAfterEnable).not.toBe(0); + }); + + datalensTest('Table rows are displayed', async ({page}) => { + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + const rowsCount = await page.locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)).count(); + + expect(rowsCount).toBeGreaterThan(0); + }); + + datalensTest('Field name can be renamed', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const {newValue} = await datasetPage.renameFirstField(); + + const updatedValue = await fieldInput.inputValue(); + expect(updatedValue).toBe(newValue); + }); + + datalensTest('Field description can be edited', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + const descriptionInput = page + .locator(slct(DatasetFieldsTabQa.FieldDescriptionColumnInput)) + .first() + .locator('input'); + + const newValue = 'test description'; + + await descriptionInput.fill(newValue); + const promise = datasetPage.waitForSuccessfulResponse(GET_PREVIEW_URL); + await page.keyboard.press('Enter'); + await promise; + + const updatedValue = await descriptionInput.inputValue(); + expect(updatedValue).toBe(newValue); + }); + + datalensTest('Hidden field is restored when visibility toggled again', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + + const hiddenBtn = page.locator(slct(DatasetFieldsTabQa.FieldVisibleColumnIcon)).first(); + + // Hide the field + const hidePromise = datasetPage.waitForSuccessfulResponse(GET_PREVIEW_URL); + await hiddenBtn.click(); + await hidePromise; + // Click again to unhide + const unhidePromise = datasetPage.waitForSuccessfulResponse(GET_PREVIEW_URL); + await hiddenBtn.click(); + await unhidePromise; + // Enable "Show hidden" to make sure hidden fields would show + const settingsBtn = page.locator(slct(DatasetFieldsTabQa.TableSettingsBtn)); + await settingsBtn.click(); + const showHiddenMenuItem = await page.waitForSelector( + slct(DatasetEditorTableSettingsItems.ShowHidden), + ); + await showHiddenMenuItem.click(); + + // All rows should still be visible since we toggled back + const rowsCountAfter = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfter).toBe(rowsCountBefore); + }); + + datalensTest('Context menu opens for a field row', async ({page}) => { + await openTestPage(page, url); + const input = await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await input.hover(); + + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const popup = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuPopup)); + await expect(popup).toBeVisible(); + + const menuItems = popup.locator('li'); + const menuItemsCount = await menuItems.count(); + expect(menuItemsCount).toBeGreaterThan(0); + }); + + datalensTest('Field can be duplicated via context menu', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + const input = await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await input.hover(); + + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const popup = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuPopup)); + await expect(popup).toBeVisible(); + + const duplicateItem = popup.locator(slct(DatasetFieldContextMenuItemsQA.DUPLICATE)); + const promise = datasetPage.waitForSuccessfulResponse(GET_PREVIEW_URL); + await duplicateItem.click(); + await promise; + + const rowsCountAfter = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfter).toBe(rowsCountBefore + 1); + }); + + datalensTest('Field can be removed via context menu', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + const input = await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await input.hover(); + + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const popup = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuPopup)); + await expect(popup).toBeVisible(); + + const removeItem = popup.locator(slct(DatasetFieldContextMenuItemsQA.REMOVE)); + const promise = datasetPage.waitForSuccessfulResponse(GET_PREVIEW_URL); + await removeItem.click(); + await promise; + + const rowsCountAfter = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfter).toBe(rowsCountBefore - 1); + }); + + datalensTest('Source column button opens field editor', async ({page}) => { + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + const sourceBtn = page.locator(slct(DatasetFieldsTabQa.FieldSourceColumnBtn)).first(); + await sourceBtn.click(); + + const fieldEditorDialog = page.locator(slct(FieldEditorQa.Dialog)); + await expect(fieldEditorDialog).toBeVisible(); + + const cancelBtn = page.locator(slct(DialogFieldEditorQA.CancelButton)); + await cancelBtn.click(); + + await expect(fieldEditorDialog).not.toBeVisible(); + }); + + datalensTest('Field name input shows error for invalid field', async ({page}) => { + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + const fieldInputValue = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .first() + .locator('input') + .inputValue(); + + const secondField = page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .nth(1) + .locator('input'); + + await secondField.fill(fieldInputValue); + const validatePromise = getValidatePromise(page); + await page.keyboard.press('Enter'); + await validatePromise; + + const inputWrapper = page.locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)).nth(1); + const hasError = await inputWrapper.locator('[aria-invalid=true]').count(); + expect(hasError).toBeGreaterThan(0); + }); + + datalensTest('Row checkboxes can be selected', async ({page}) => { + await openTestPage(page, url); + const input = await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await input.hover(); + + const checkbox = page.locator(slct(DatasetFieldsTabQa.FieldIndexColumnCheckbox)).first(); + await checkbox.click(); + + const isChecked = await checkbox.locator('input[type="checkbox"]').isChecked(); + expect(isChecked).toBe(true); + }); + + datalensTest('Select all checkbox selects all rows', async ({page}) => { + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + const rowsCount = await page.locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)).count(); + + // Click "select all" checkbox in the header + const selectAllCheckbox = page + .locator(slct(DatasetFieldsTabQa.FieldIndexHeaderColumnCheckbox)) + .first(); + await selectAllCheckbox.click(); + + const checkedCheckboxes = page + .locator(slct(DatasetFieldsTabQa.FieldIndexColumnCheckbox)) + .locator('input[type="checkbox"]:checked'); + const checkedCount = await checkedCheckboxes.count(); + expect(checkedCount).toBe(rowsCount); + }); + + datalensTest('Batch action panel appears when rows are selected', async ({page}) => { + await openTestPage(page, url); + const input = await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await input.hover(); + + // Select a row + const checkbox = page.locator(slct(DatasetFieldsTabQa.FieldIndexColumnCheckbox)).first(); + await checkbox.click(); + + // Batch action panel should appear + const actionsPanel = page.locator(slct(DatasetFieldsTabQa.BatchActionsPanel)); + await expect(actionsPanel).toBeVisible(); + }); + + datalensTest('Batch action panel disappears when selection is cleared', async ({page}) => { + await openTestPage(page, url); + const input = await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await input.hover(); + + // Select a row + const checkbox = page.locator(slct(DatasetFieldsTabQa.FieldIndexColumnCheckbox)).first(); + await checkbox.click(); + + const actionsPanel = page.locator(slct(DatasetFieldsTabQa.BatchActionsPanel)); + await expect(actionsPanel).toBeVisible(); + + // Deselect the row + await checkbox.click(); + + await expect(actionsPanel).not.toBeVisible(); + }); +}); diff --git a/tests/opensource-suites/dataset/base/preview.test.ts b/tests/opensource-suites/dataset/base/preview.test.ts new file mode 100644 index 0000000000..3daa9f68f8 --- /dev/null +++ b/tests/opensource-suites/dataset/base/preview.test.ts @@ -0,0 +1,112 @@ +import {expect} from '@playwright/test'; + +import datalensTest from '../../../utils/playwright/globalTestDefinition'; +import {openTestPage, slct} from '../../../utils'; +import { + DatasetPreviewQA, + DatasetPanelQA, + DATASET_TAB, + DatasetActionQA, + DatasetFieldsTabQa, + DatasetSourcesTableQa, +} from '../../../../src/shared'; +import {DatasetsEntities} from '../../../constants/test-entities/datasets'; +import DatasetPage from '../../../page-objects/dataset/DatasetPage'; +import {SET_CONNECTION_METHODS} from '../../../page-objects/dataset/constants'; +import {ConnectionsNames} from '../../../constants/test-entities/connections'; +import {WorkbookEntities} from '../../../constants/test-entities/workbook'; + +datalensTest.describe('Dataset basic ui', () => { + const url = `datasets${DatasetsEntities.Basic.url}`; + + datalensTest('Preview table should be visible and can be hide', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + const selector = slct(DatasetPreviewQA.Preview); + await page.waitForSelector(selector); + const preview = page.locator(selector); + await expect(preview).toBeVisible(); + await datasetPage.openTab(DATASET_TAB.SOURCES); + await expect(preview).toBeVisible(); + const previewBtn = page.locator(slct(DatasetPanelQA.PreviewButton)); + await previewBtn.click(); + await expect(preview).not.toBeVisible(); + await datasetPage.openTab(DATASET_TAB.DATASET); + await expect(preview).not.toBeVisible(); + }); + + datalensTest('Preview table should can change row count', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + const selector = slct(DatasetPreviewQA.RowCountInput); + await page.waitForSelector(selector); + const rowInput = page.locator(selector).locator('input'); + await rowInput.press('Meta+A'); + await rowInput.press('Backspace'); + await rowInput.fill('1'); + await datasetPage.waitForSuccessfulResponse('/getPreview'); + + const previewRows = await page + .locator(`${slct(DatasetPreviewQA.Preview)} table tbody tr`) + .count(); + expect(previewRows).toBe(1); + }); + + datalensTest('Preview table should be closed on close btn', async ({page}) => { + await openTestPage(page, url); + const selector = slct(DatasetPreviewQA.ClosePreviewBtn); + await page.waitForSelector(selector); + const previewCloseBtn = page.locator(selector); + await previewCloseBtn.click(); + const preview = page.locator(DatasetPreviewQA.Preview); + await expect(preview).not.toBeVisible(); + }); + + datalensTest('Preview response should not be called when preview closed', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + const selector = slct(DatasetPreviewQA.ClosePreviewBtn); + await page.waitForSelector(selector); + const previewCloseBtn = page.locator(selector); + await previewCloseBtn.click(); + const preview = page.locator(DatasetPreviewQA.Preview); + await expect(preview).not.toBeVisible(); + let previewRequested = false; + page.on('request', (request) => { + if (request.url().includes('/getPreview')) { + previewRequested = true; + } + }); + + await datasetPage.renameFirstField(); + await page.waitForLoadState('networkidle'); + + expect(previewRequested).toBe(false); + }); + + datalensTest('Global preview disable should work in dataset', async ({page}) => { + const datasetPage = new DatasetPage({page}); + await openTestPage(page, `${WorkbookEntities.Basic.url}/datasets/new`); + await datasetPage.setConnectionInWorkbookDataset({ + method: SET_CONNECTION_METHODS.ADD, + connectionName: ConnectionsNames.ConnectionPostgreSQL, + }); + await page.waitForSelector(slct(DatasetSourcesTableQa.Source)); + await datasetPage.addAvatarByDragAndDrop(); + await datasetPage.createDatasetInWorkbookOrCollection(); + const selector = slct(DatasetPreviewQA.Preview); + await page.waitForSelector(selector); + const preview = page.locator(selector); + await expect(preview).toBeVisible(); + const datasetSettingsBtn = page.locator(slct(DatasetActionQA.SettingsButton)); + await datasetSettingsBtn.click(); + const showPreviewSelectItem = await page.waitForSelector( + slct(DatasetActionQA.SettingsShowPreviewByDefault), + ); + await showPreviewSelectItem.click(); + await datasetPage.saveUpdatedDataset(); + await page.reload(); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + await expect(preview).not.toBeVisible(); + }); +}); diff --git a/tests/opensource-suites/dataset/base/sources.test.ts b/tests/opensource-suites/dataset/base/sources.test.ts new file mode 100644 index 0000000000..48929069c9 --- /dev/null +++ b/tests/opensource-suites/dataset/base/sources.test.ts @@ -0,0 +1,99 @@ +import {expect} from '@playwright/test'; + +import datalensTest from '../../../utils/playwright/globalTestDefinition'; +import {openTestPage, slct} from '../../../utils'; +import { + DATASET_TAB, + DatasetSourcesTableQa, + DatasetSourceEditorDialogQA, +} from '../../../../src/shared'; +import {DatasetsEntities} from '../../../constants/test-entities/datasets'; +import {getValidatePromise} from '../../../page-objects/dataset/utils'; + +datalensTest.describe('Dataset sources', () => { + datalensTest('Add source button should add source', async ({page}) => { + const url = `datasets${DatasetsEntities.Basic.url}`; + await openTestPage(page, url, {tab: DATASET_TAB.SOURCES}); + + await page.waitForSelector(slct(DatasetSourcesTableQa.Source)); + const startSourcesCount = await page.locator(slct(DatasetSourcesTableQa.Source)).count(); + + const addSourceBtn = page.locator(slct(DatasetSourcesTableQa.SourcesAddItemBtn)); + await addSourceBtn.click(); + + const tableNameInput = page + .locator(slct(DatasetSourceEditorDialogQA.EditPathInput)) + .first() + .locator('input'); + await tableNameInput.fill('foobar'); + const schemeNameInput = page + .locator(slct(DatasetSourceEditorDialogQA.EditPathInput)) + .nth(1) + .locator('input'); + await schemeNameInput.fill('barfoo'); + + const applyBtn = page.locator(slct(DatasetSourceEditorDialogQA.ApplyBtn)); + const validatePromise = getValidatePromise(page); + await applyBtn.click(); + await validatePromise; + + const closeBtn = page.locator(slct(DatasetSourceEditorDialogQA.CancelBtn)); + await closeBtn.click(); + + const endSourcesCount = await page.locator(slct(DatasetSourcesTableQa.Source)).count(); + expect(endSourcesCount).toEqual(startSourcesCount + 1); + }); + + datalensTest('Add source button should add SQL source', async ({page}) => { + const url = `datasets${DatasetsEntities.Basic.url}`; + await openTestPage(page, url, {tab: DATASET_TAB.SOURCES}); + await page.waitForSelector(slct(DatasetSourcesTableQa.Source)); + + const startSourcesCount = await page.locator(slct(DatasetSourcesTableQa.Source)).count(); + + const addSourceBtn = page.locator(slct(DatasetSourcesTableQa.SourcesAddItemBtn)); + await addSourceBtn.click(); + const editorDialog = page.locator(slct(DatasetSourceEditorDialogQA.Dialog)); + const editorSwitch = page.locator(slct(DatasetSourceEditorDialogQA.SourceEditorSwitch)); + const sqlSwitch = editorSwitch.locator('label', {hasText: 'SQL'}).locator('input'); + await sqlSwitch.click(); + + const editor = editorDialog.locator('.react-monaco-editor-container'); + await editor.click(); + await page.keyboard.insertText('abc'); + + const applyBtn = page.locator(slct(DatasetSourceEditorDialogQA.ApplyBtn)); + const validatePromise = getValidatePromise(page); + await applyBtn.click(); + await validatePromise; + + const endSourcesCount = await page.locator(slct(DatasetSourcesTableQa.Source)).count(); + expect(endSourcesCount).toEqual(startSourcesCount + 1); + }); + + datalensTest('Source table has not add source button', async ({page}) => { + const url = `datasets${DatasetsEntities.WithOtherConnection.url}`; + await openTestPage(page, url, {tab: DATASET_TAB.SOURCES}); + + await page.waitForSelector(slct(DatasetSourcesTableQa.Source)); + const addSourceBtn = page.locator(slct(DatasetSourcesTableQa.SourcesAddItemBtn)); + await expect(addSourceBtn).not.toBeVisible(); + }); + + datalensTest('Source edit SQL switch should be disabled', async ({page}) => { + const url = `datasets${DatasetsEntities.WithOtherConnection.url}`; + await openTestPage(page, url, {tab: DATASET_TAB.SOURCES}); + + await page.waitForSelector(slct(DatasetSourcesTableQa.Source)); + const sourceContextMenu = page.locator(slct(DatasetSourcesTableQa.SourceContextMenuBtn)); + await sourceContextMenu.click(); + const editBtn = page.locator(slct(DatasetSourcesTableQa.SourceContextMenuModify)); + await editBtn.click(); + const editorDialog = page.locator(slct(DatasetSourceEditorDialogQA.Dialog)); + await expect(editorDialog).toBeVisible(); + const editorSwitch = page.locator(slct(DatasetSourceEditorDialogQA.SourceEditorSwitch)); + const sqlSwitch = editorSwitch.locator('label', {hasText: 'SQL'}).locator('input'); + await expect(sqlSwitch).toBeVisible(); + await expect(sqlSwitch).toBeDisabled(); + }); +}); diff --git a/tests/opensource-suites/dataset/base/tabs.test.ts b/tests/opensource-suites/dataset/base/tabs.test.ts new file mode 100644 index 0000000000..83cbcc8c28 --- /dev/null +++ b/tests/opensource-suites/dataset/base/tabs.test.ts @@ -0,0 +1,64 @@ +import {expect} from '@playwright/test'; + +import datalensTest from '../../../utils/playwright/globalTestDefinition'; +import {openTestPage, slct} from '../../../utils'; +import { + DatasetSourcesTableQa, + DatasetFieldsTabQa, + ParametersQA, + FiltersQA, + RelationsMapQA, +} from '../../../../src/shared'; +import {DatasetsEntities} from '../../../constants/test-entities/datasets'; +import {RobotChartsDatasetUrls} from '../../../utils/constants'; + +datalensTest.describe('Dataset basic ui', () => { + const url = `datasets${DatasetsEntities.Basic.url}`; + datalensTest('Sources tab should be open with query', async ({page}) => { + await openTestPage(page, url, {tab: 'sources'}); + const selection = slct(DatasetSourcesTableQa.Source); + await page.waitForSelector(selection); + const source = page.locator(selection).first(); + await expect(source).toBeVisible(); + }); + + datalensTest('Dataset tab should be open with query', async ({page}) => { + await openTestPage(page, url, {tab: 'dataset'}); + const selection = slct(DatasetFieldsTabQa.FieldNameColumnInput); + await page.waitForSelector(selection); + const fieldInput = page.locator(selection).first(); + await expect(fieldInput).toBeVisible(); + }); + + datalensTest('Parameters tab should be open with query', async ({page}) => { + await openTestPage(page, url, {tab: 'parameters'}); + const selection = slct(ParametersQA.ParametersTabSection); + await page.waitForSelector(selection); + const parametersTab = page.locator(selection); + await expect(parametersTab).toBeVisible(); + }); + + datalensTest('Filters tab should be open with query', async ({page}) => { + await openTestPage(page, url, {tab: 'filters'}); + const selection = slct(FiltersQA.FiltersTabSection); + await page.waitForSelector(selection); + const filtersTab = page.locator(selection); + await expect(filtersTab).toBeVisible(); + }); + + datalensTest('Dataset tab should be open with wrong query', async ({page}) => { + await openTestPage(page, url, {tab: 'foobar'}); + const selection = slct(DatasetFieldsTabQa.DatasetEditor); + await page.waitForSelector(selection); + const relationsMap = page.locator(selection); + await expect(relationsMap).toBeVisible(); + }); + + datalensTest('Sources tab should be open in new dataset', async ({page}) => { + await openTestPage(page, RobotChartsDatasetUrls.NewDataset); + const selection = slct(RelationsMapQA.RelationsMap); + await page.waitForSelector(selection); + const relationsMap = page.locator(selection); + await expect(relationsMap).toBeVisible(); + }); +}); diff --git a/tests/opensource-suites/dataset/base/unload-alert.test.ts b/tests/opensource-suites/dataset/base/unload-alert.test.ts new file mode 100644 index 0000000000..32e2deb62d --- /dev/null +++ b/tests/opensource-suites/dataset/base/unload-alert.test.ts @@ -0,0 +1,99 @@ +import {expect} from '@playwright/test'; +import {v4 as uuidv4} from 'uuid'; + +import datalensTest from '../../../utils/playwright/globalTestDefinition'; +import {openTestPage, slct} from '../../../utils'; +import {DatasetFieldsTabQa} from '../../../../src/shared'; +import {DatasetsEntities} from '../../../constants/test-entities/datasets'; +import DatasetPage from '../../../page-objects/dataset/DatasetPage'; + +datalensTest.describe('Dataset unsaved changes navigation prevention', () => { + const url = `datasets${DatasetsEntities.Basic.url}`; + let datasetPage: DatasetPage; + + datalensTest.beforeEach(async ({page}) => { + datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + }); + + datalensTest('No dialog appears on page reload when no changes were made', async ({page}) => { + let dialogAppeared = false; + page.on('dialog', async (dialog) => { + dialogAppeared = true; + await dialog.accept(); + }); + + await page.reload(); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + expect(dialogAppeared).toBe(false); + }); + + datalensTest( + 'beforeunload dialog appears on page reload after making changes', + async ({page}) => { + await datasetPage.renameFirstField({value: uuidv4()}); + + const dialogPromise = page.waitForEvent('dialog'); + // Do not await reload — dismiss will cancel it + page.reload().catch(() => {}); + + const dialog = await dialogPromise; + expect(dialog.type()).toBe('beforeunload'); + await dialog.dismiss(); + }, + ); + + datalensTest('Dismissing beforeunload dialog keeps user on the page', async ({page}) => { + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const {newValue} = (await datasetPage.renameFirstField({value: uuidv4()})) ?? {}; + + const dialogPromise = page.waitForEvent('dialog'); + page.reload().catch(() => {}); + + const dialog = await dialogPromise; + await dialog.dismiss(); + + // URL should still contain the dataset path + expect(page.url()).toContain(DatasetsEntities.Basic.url); + + // The modified value should still be present (page was not reloaded) + const currentValue = await fieldInput.inputValue(); + expect(currentValue).toBe(newValue); + }); + + datalensTest('Accepting beforeunload dialog allows the page to reload', async ({page}) => { + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const {originalValue} = (await datasetPage.renameFirstField({value: uuidv4()})) ?? {}; + + page.once('dialog', (dialog) => dialog.accept()); + + await page.reload(); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + // After reload, unsaved changes are lost — field reverts to original value + const currentValue = await fieldInput.inputValue(); + expect(currentValue).toBe(originalValue); + }); + + datalensTest('No dialog appears on page reload after saving changes', async ({page}) => { + const url = `datasets${DatasetsEntities.WithOtherConnection.url}`; + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + await datasetPage.renameFirstField({value: uuidv4()}); + await datasetPage.saveUpdatedDataset(); + + let dialogAppeared = false; + page.on('dialog', async (dialog) => { + dialogAppeared = true; + await dialog.accept(); + }); + + await page.reload(); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + + expect(dialogAppeared).toBe(false); + }); +}); diff --git a/tests/opensource-suites/dataset/filters/dataset-filters.test.ts b/tests/opensource-suites/dataset/filters/dataset-filters.test.ts new file mode 100644 index 0000000000..474bee7de0 --- /dev/null +++ b/tests/opensource-suites/dataset/filters/dataset-filters.test.ts @@ -0,0 +1,120 @@ +import {Page, expect} from '@playwright/test'; + +import datalensTest from '../../../utils/playwright/globalTestDefinition'; +import {openTestPage, slct, waitForCondition} from '../../../utils'; +import {DialogFilterQA, FiltersQA, Operations} from '../../../../src/shared'; +import {DatasetsEntities} from '../../../constants/test-entities/datasets'; +import DatasetPage from '../../../page-objects/dataset/DatasetPage'; +import {VALIDATE_DATASET_URL} from '../../../page-objects/dataset/constants'; +import {getValidatePromise} from '../../../page-objects/dataset/utils'; + +async function addIsNullFilter(page: Page, datasetPage: DatasetPage) { + await datasetPage.datasetTabSection.clickAddButton(); + + const fieldItems = page.locator(slct(DialogFilterQA.ListItem)); + const fieldItemsCount = await fieldItems.count(); + const fieldItem = fieldItems.first(); + const fieldItemText = await fieldItem.textContent(); + await fieldItem.click(); + + const operationSelect = page.locator(slct(DialogFilterQA.OperationSelect)); + await expect(operationSelect).toBeVisible(); + await operationSelect.click(); + await page.click(slct(Operations.ISNULL)); + + const validatePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await page.click(slct(DialogFilterQA.ApplyButton)); + await validatePromise; + let rowsCount; + await waitForCondition(async () => { + rowsCount = await datasetPage.datasetTabSection.getRowsCount(); + return rowsCount === 1; + }); + return {rowsCount, fieldItemText, fieldItemsCount}; +} + +datalensTest.describe('Dataset obligatory filters', () => { + const url = `datasets${DatasetsEntities.WithOtherConnection.url}`; + let datasetPage: DatasetPage; + datalensTest.beforeEach(async ({page}) => { + datasetPage = new DatasetPage({page}); + await openTestPage(page, url, {tab: 'filters'}); + await page.waitForSelector(slct(FiltersQA.FiltersTabSection)); + }); + + datalensTest('Adding an obligatory filter to a dataset', async ({page}) => { + const initialRowsCount = await datasetPage.datasetTabSection.getRowsCount(); + + const {rowsCount} = await addIsNullFilter(page, datasetPage); + expect(rowsCount).toEqual(initialRowsCount + 1); + }); + + datalensTest('Filter row displays field name and operation preview', async ({page}) => { + const {rowsCount, fieldItemText} = await addIsNullFilter(page, datasetPage); + expect(rowsCount).toEqual(1); + + // Verify the filter row content + const row = await datasetPage.datasetTabSection.getRowLocatorByIndex(0); + await expect(row).toContainText(fieldItemText!); + await expect(row).toContainText(Operations.ISNULL); + }); + + datalensTest('Editing an obligatory filter', async ({page}) => { + // First add a filter with ISNULL + await addIsNullFilter(page, datasetPage); + + // Click on the filter row to open edit dialog + const row = await datasetPage.datasetTabSection.getRowLocatorByIndex(0); + await row.click(); + + const dialog = page.locator(slct(DialogFilterQA.Dialog)); + await expect(dialog).toBeVisible(); + + // Change operation from ISNULL to ISNOTNULL + const editOperationSelect = page.locator(slct(DialogFilterQA.OperationSelect)); + await editOperationSelect.click(); + await page.click(slct(Operations.ISNOTNULL)); + + const validatePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await page.click(slct(DialogFilterQA.ApplyButton)); + await validatePromise; + + // Verify the filter row updated + const updatedRow = await datasetPage.datasetTabSection.getRowLocatorByIndex(0); + await expect(updatedRow).toContainText(Operations.ISNOTNULL); + }); + + datalensTest('Deleting an obligatory filter', async ({page}) => { + // First add a filter + await addIsNullFilter(page, datasetPage); + + // Click the delete button on the filter row + const row = await datasetPage.datasetTabSection.getRowLocatorByIndex(0); + const deleteButton = row.locator(slct(FiltersQA.TableDeleteRowBtn)); + + const validatePromise = getValidatePromise(page); + await deleteButton.click(); + await validatePromise; + + await waitForCondition(async () => { + const rowsCount = await datasetPage.datasetTabSection.getRowsCount(); + return rowsCount === 0; + }).catch(() => { + throw new Error('The filter row was not deleted'); + }); + }); + + datalensTest( + 'Added filter is not available in the field list for new filter', + async ({page}) => { + const {fieldItemsCount} = await addIsNullFilter(page, datasetPage); + // Open dialog again and check that the field list has one fewer item + await datasetPage.datasetTabSection.clickAddButton(); + const dialog = page.locator(slct(DialogFilterQA.Dialog)); + await expect(dialog).toBeVisible(); + + const updatedFieldsCount = await page.locator(slct(DialogFilterQA.ListItem)).count(); + expect(updatedFieldsCount).toBe(fieldItemsCount - 1); + }, + ); +}); diff --git a/tests/page-objects/dataset/DatasetConnectionSection.ts b/tests/page-objects/dataset/DatasetConnectionSection.ts new file mode 100644 index 0000000000..0621c1b83f --- /dev/null +++ b/tests/page-objects/dataset/DatasetConnectionSection.ts @@ -0,0 +1,108 @@ +import {Page} from '@playwright/test'; +import {slct} from '../../utils'; +import {getValidatePromise} from './utils'; +import { + AvatarQA, + DatasetSourcesLeftPanelQA, + DatasetSourcesTableQa, + ValueOf, +} from '../../../src/shared'; +import {SET_CONNECTION_METHODS} from './constants'; + +export type SetConnectionProps = { + connectionName: string; + method: ValueOf; +}; + +export default class DatasetConnectionSection { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async openCurrentConnection() { + const newTabPagePromise: Promise = new Promise((resolve) => + this.page.context().on('page', resolve), + ); + const contextMenu = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnContextMenuBtn), + ); + await contextMenu.click(); + const openConnectionBtn = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnContextMenuOpen), + ); + await openConnectionBtn.click(); + const newPage = await newTabPagePromise; + return newPage; + } + + async openConnectionSelectViaMethod(method: SetConnectionProps['method']) { + switch (method) { + case SET_CONNECTION_METHODS.ADD: { + await this.addConnection(); + break; + } + case SET_CONNECTION_METHODS.REPLACE: { + await this.replaceConnection(); + break; + } + case SET_CONNECTION_METHODS.DELETE: { + await this.deleteConnection(); + await this.addConnection(); + break; + } + } + } + + async deleteConnection() { + await this.page.waitForSelector(slct(AvatarQA.Avatar)); + await this.page.waitForSelector(slct(DatasetSourcesTableQa.Source)); + const deleteAvatarSelector = slct(AvatarQA.DeleteButton); + const sourceMenuSelector = slct(DatasetSourcesTableQa.SourceContextMenuBtn); + + // delete all avatars + while ((await this.page.locator(deleteAvatarSelector).count()) > 0) { + const validatePromise = getValidatePromise(this.page); + await this.page.locator(deleteAvatarSelector).first().click(); + await validatePromise; + } + // delete all sources + while ((await this.page.locator(sourceMenuSelector).count()) > 0) { + await this.page.locator(sourceMenuSelector).first().click(); + const validatePromise = getValidatePromise(this.page); + await this.page + .locator(slct(DatasetSourcesTableQa.SourceContextMenuDelete)) + .first() + .click(); + await validatePromise; + } + + const contextMenu = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnContextMenuBtn), + ); + await contextMenu.click(); + const deleteBtn = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnContextMenuDelete), + ); + await deleteBtn.click(); + } + + private async addConnection() { + const connSelectionButton = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnSelection), + ); + await connSelectionButton.click(); + } + + private async replaceConnection() { + const contextMenu = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnContextMenuBtn), + ); + await contextMenu.click(); + const replaceBtn = await this.page.waitForSelector( + slct(DatasetSourcesLeftPanelQA.ConnContextMenuReplace), + ); + await replaceBtn.click(); + } +} diff --git a/tests/page-objects/dataset/DatasetFiledsTable.ts b/tests/page-objects/dataset/DatasetFiledsTable.ts new file mode 100644 index 0000000000..f9e98c68d0 --- /dev/null +++ b/tests/page-objects/dataset/DatasetFiledsTable.ts @@ -0,0 +1,75 @@ +import {expect, Locator, Page} from '@playwright/test'; +import {slct} from '../../utils'; +import {DatasetFieldsTabQa} from '../../../src/shared/constants/qa/datasets'; + +export default class DatasetFieldsTable { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + getFieldNameInput() { + return this.page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .first() + .locator('input'); + } + + async changeFieldSelect(locator: Locator) { + // Get the type select control for the first field + const selectBtns = locator; + const selectBtnsCount = await selectBtns.count(); + let targetOption = null; + let targetOptionText = ''; + let selectBtn = null; + let originalSelectText; + for (let i = 0; i < selectBtnsCount; i++) { + selectBtn = selectBtns.nth(i); + originalSelectText = await selectBtn.textContent(); + + // Click the type selector to open dropdown + await selectBtn.click(); + + // Wait for the select popup and pick a different type + const selectPopup = this.page.locator(slct('select-popup')); + await expect(selectPopup).toBeVisible(); + + // Get all options and pick a different one + const options = selectPopup.locator('[role="option"]'); + const optionsCount = await options.count(); + + // Find an option that has different text from the current type + + for (let i = 0; i < optionsCount; i++) { + const optionText = await options.nth(i).textContent(); + if (optionText !== originalSelectText) { + targetOption = options.nth(i); + targetOptionText = optionText || ''; + break; + } + } + + if (targetOption) { + break; + } else { + await selectBtn.click(); + } + } + return { + targetOption, + selectBtn, + originalSelectText, + targetOptionText, + }; + } + + async selectTwoCheckboxes() { + const inputs = this.page.locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + const checkboxes = this.page.locator(slct(DatasetFieldsTabQa.FieldIndexColumnCheckbox)); + await inputs.first().hover(); + await checkboxes.first().click(); + await inputs.nth(1).hover(); + await checkboxes.nth(1).click(); + } +} diff --git a/tests/page-objects/dataset/DatasetPage.ts b/tests/page-objects/dataset/DatasetPage.ts index 3c6e9990ef..8dfaa55da4 100644 --- a/tests/page-objects/dataset/DatasetPage.ts +++ b/tests/page-objects/dataset/DatasetPage.ts @@ -1,3 +1,4 @@ +import {Page, Response, expect} from '@playwright/test'; import {v1 as uuidv1} from 'uuid'; import { DialogCreateWorkbookEntryQa, @@ -8,25 +9,27 @@ import { DatasetActionQA, DatasetSourcesTableQa, DatasetSourcesLeftPanelQA, - AvatarQA, - DatasetFieldsTabQa, } from '../../../src/shared/constants/qa/datasets'; - -import {deleteEntity, slct} from '../../utils'; -import {BasePage, BasePageProps} from '../BasePage'; -import DialogParameter from '../common/DialogParameter'; - -import DatasetTabSection from './DatasetTabSection'; import { CollectionFiltersQa, DialogCollectionStructureQa, SharedEntriesBaseQa, SharedEntriesPermissionsDialogQa, ValueOf, + DATASET_TAB, } from '../../../src/shared'; -import {Page, Response, expect} from '@playwright/test'; + +import {deleteEntity, slct} from '../../utils'; +import {BasePage, BasePageProps} from '../BasePage'; +import DialogParameter from '../common/DialogParameter'; import Revisions from '../common/Revisions'; +import DatasetTabSection from './DatasetTabSection'; +import DatasetConnectionSection, {SetConnectionProps} from './DatasetConnectionSection'; +import DatasetFieldsTable from './DatasetFiledsTable'; +import {VALIDATE_DATASET_URL} from './constants'; +import {NavigationMinimalPopup} from '../workbook/NavigationMinimalPopup'; + export interface DatasetPageProps extends BasePageProps {} export const waitForBiValidateDatasetResponses = (page: Page, timeout: number): Promise => { @@ -65,20 +68,19 @@ export const waitForBiValidateDatasetResponses = (page: Page, timeout: number): }); }; -export const SET_CONNECTION_METHODS = { - ADD: 'add', - REPLACE: 'replace', - DELETE: 'delete', -} as const; - class DatasetPage extends BasePage { datasetTabSection: DatasetTabSection; + datasetConnectionSection: DatasetConnectionSection; + datasetFieldsTable: DatasetFieldsTable; + workbookNavigationMinimal: NavigationMinimalPopup; dialogParameter: DialogParameter; revisions: Revisions; constructor({page}: DatasetPageProps) { super({page}); - + this.workbookNavigationMinimal = new NavigationMinimalPopup(page); + this.datasetConnectionSection = new DatasetConnectionSection(page); + this.datasetFieldsTable = new DatasetFieldsTable(page); this.datasetTabSection = new DatasetTabSection(page); this.dialogParameter = new DialogParameter(page); this.revisions = new Revisions(page); @@ -105,8 +107,8 @@ class DatasetPage extends BasePage { await this.page.dragAndDrop(selector, targetSelector); } - async openSourcesPanel() { - await this.page.click('.dataset-panel input[value=sources]'); + async openTab(tab: ValueOf) { + await this.page.click(`.dataset-panel input[value=${tab}]`); } async createDatasetInWorkbookOrCollection({ @@ -208,85 +210,14 @@ class DatasetPage extends BasePage { return dsName; } - async openCurrentConnection() { - const newTabPagePromise: Promise = new Promise((resolve) => - this.page.context().on('page', resolve), - ); - const contextMenu = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnContextMenuBtn), - ); - await contextMenu.click(); - const openConnectionBtn = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnContextMenuOpen), - ); - await openConnectionBtn.click(); - const newPage = await newTabPagePromise; - return newPage; + async setConnectionInWorkbookDataset({method, connectionName}: SetConnectionProps) { + await this.datasetConnectionSection.openConnectionSelectViaMethod(method); + await this.workbookNavigationMinimal.fillInput(connectionName); + await this.workbookNavigationMinimal.selectListItem({innerText: connectionName}); } - async setSharedConnection({ - connectionName, - method, - }: { - connectionName: string; - method: ValueOf; - }) { - switch (method) { - case SET_CONNECTION_METHODS.ADD: { - const connSelectionButton = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnSelection), - ); - await connSelectionButton.click(); - break; - } - case SET_CONNECTION_METHODS.REPLACE: { - const contextMenu = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnContextMenuBtn), - ); - await contextMenu.click(); - const replaceBtn = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnContextMenuReplace), - ); - await replaceBtn.click(); - break; - } - case SET_CONNECTION_METHODS.DELETE: { - await this.page.waitForSelector(slct(AvatarQA.Avatar)); - await this.page.waitForSelector(slct(DatasetSourcesTableQa.Source)); - const deleteAvatarSelector = slct(AvatarQA.DeleteButton); - const sourceMenuSelector = slct(DatasetSourcesTableQa.SourceContextMenuBtn); - - // delete all avatars - while ((await this.page.locator(deleteAvatarSelector).count()) > 0) { - await this.page.locator(deleteAvatarSelector).first().click(); - await waitForBiValidateDatasetResponses(this.page, 5000); - } - // delete all sources - while ((await this.page.locator(sourceMenuSelector).count()) > 0) { - await this.page.locator(sourceMenuSelector).first().click(); - await this.page - .locator(slct(DatasetSourcesTableQa.SourceContextMenuDelete)) - .first() - .click(); - await waitForBiValidateDatasetResponses(this.page, 5000); - } - - const contextMenu = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnContextMenuBtn), - ); - await contextMenu.click(); - const deleteBtn = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnContextMenuDelete), - ); - await deleteBtn.click(); - const connSelectionButton = await this.page.waitForSelector( - slct(DatasetSourcesLeftPanelQA.ConnSelection), - ); - await connSelectionButton.click(); - break; - } - } - + async setSharedConnection({connectionName, method}: SetConnectionProps) { + await this.datasetConnectionSection.openConnectionSelectViaMethod(method); await this.page.waitForSelector(slct(CollectionFiltersQa.SearchInput)); const search = this.page.locator(slct(CollectionFiltersQa.SearchInput)).locator('input'); await search.press('Meta+A'); @@ -337,13 +268,15 @@ class DatasetPage extends BasePage { } async renameFirstField({value}: {value?: string} = {}) { - const fieldInput = this.page.locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)).first(); - const originalValue = await fieldInput.locator('input').inputValue(); + const fieldInput = this.datasetFieldsTable.getFieldNameInput(); + const originalValue = await fieldInput.inputValue(); const newValue = value || `${originalValue}_modified`; - await fieldInput.locator('input').fill(newValue); + await fieldInput.fill(newValue); + const validatePromise = this.waitForSuccessfulResponse(VALIDATE_DATASET_URL); await this.page.keyboard.press('Enter'); - await waitForBiValidateDatasetResponses(this.page, 5000); + await validatePromise; + return {newValue, originalValue}; } diff --git a/tests/page-objects/dataset/constants.ts b/tests/page-objects/dataset/constants.ts new file mode 100644 index 0000000000..f648cd9c54 --- /dev/null +++ b/tests/page-objects/dataset/constants.ts @@ -0,0 +1,7 @@ +export const GET_PREVIEW_URL = '/getPreview'; +export const VALIDATE_DATASET_URL = '/validateDataset'; +export const SET_CONNECTION_METHODS = { + ADD: 'add', + REPLACE: 'replace', + DELETE: 'delete', +} as const; diff --git a/tests/page-objects/dataset/utils.ts b/tests/page-objects/dataset/utils.ts new file mode 100644 index 0000000000..47b161a647 --- /dev/null +++ b/tests/page-objects/dataset/utils.ts @@ -0,0 +1,5 @@ +import {Page} from '@playwright/test'; +import {VALIDATE_DATASET_URL} from './constants'; + +export const getValidatePromise = (page: Page) => + page.waitForResponse((response) => response.url().includes(VALIDATE_DATASET_URL)); diff --git a/tests/page-objects/wizard/FieldEditor.ts b/tests/page-objects/wizard/FieldEditor.ts index f1431dd719..be7bfe8b41 100644 --- a/tests/page-objects/wizard/FieldEditor.ts +++ b/tests/page-objects/wizard/FieldEditor.ts @@ -11,6 +11,7 @@ export default class FieldEditor { private applyButtonSelector = slct(DialogFieldEditorQA.ApplyButton); private fieldEditorSelector = FieldEditor.slct('.react-monaco-editor-container'); private fieldItemSelector = FieldEditor.slct('.g-list__item'); + private fieldNameEditBtnSelector = slct(DialogFieldEditorQA.EditNameButton); private page: Page; @@ -29,6 +30,14 @@ export default class FieldEditor { await this.page.keyboard.insertText(name); } + async changeName(name: string) { + await this.page.click(this.fieldNameEditBtnSelector); + const input = this.page.locator(this.fieldNameSelector); + await input.press('Meta+A'); + await input.press('Backspace'); + await input.fill(name); + } + async setFormula(formula: string) { await this.page.click(this.fieldEditorSelector); await this.page.keyboard.insertText(formula); diff --git a/tests/suites/dataset/history.test.ts b/tests/suites/dataset/history.test.ts new file mode 100644 index 0000000000..720212c48a --- /dev/null +++ b/tests/suites/dataset/history.test.ts @@ -0,0 +1,661 @@ +import {expect} from '@playwright/test'; + +import datalensTest from '../../utils/playwright/globalTestDefinition'; +import {openTestPage, slct} from '../../utils'; +import { + DatasetFieldsTabQa, + DatasetEditorTableSettingsItems, + EditHistoryQA, + DatasetFieldContextMenuItemsQA, + FieldEditorQa, + DatasetFieldTabBatchPanelQa, + DialogConfirmQA, +} from '../../../src/shared'; +import DatasetPage from '../../page-objects/dataset/DatasetPage'; +import FieldEditor from '../../page-objects/wizard/FieldEditor'; +import {VALIDATE_DATASET_URL} from '../../page-objects/dataset/constants'; +import {RobotChartsDatasetUrls} from '../../utils/constants'; +import {getValidatePromise} from '../../page-objects/dataset/utils'; + +datalensTest.describe('Dataset history', () => { + const url = RobotChartsDatasetUrls.DatasetWithCsvConnection; + let datasetPage: DatasetPage; + + datalensTest.beforeEach(async ({page}) => { + datasetPage = new DatasetPage({page}); + await openTestPage(page, url); + await page.waitForSelector(slct(DatasetFieldsTabQa.FieldNameColumnInput)); + // Wait for initial dataset validation to complete, ensuring the edit history is initialized + await page.waitForResponse( + (response) => response.url().includes('/validateDataset') && response.ok(), + ); + }); + + datalensTest('Undo button is disabled when no changes were made', async ({page}) => { + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeVisible(); + await expect(undoBtn).toBeDisabled(); + }); + + datalensTest('Redo button is disabled when no changes were made', async ({page}) => { + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeVisible(); + await expect(redoBtn).toBeDisabled(); + }); + + datalensTest('Undo and redo reverts a field rename', async ({page}) => { + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeDisabled(); + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const {newValue: changedValue, originalValue} = + (await datasetPage.renameFirstField()) ?? {}; + + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const undoValue = await fieldInput.inputValue(); + expect(undoValue).toBe(originalValue); + + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const redoValue = await fieldInput.inputValue(); + expect(redoValue).toBe(changedValue); + }); + + datalensTest('Redo is disabled after undo and making a new change', async ({page}) => { + await datasetPage.renameFirstField(); + + // Undo + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + + await datasetPage.renameFirstField({value: 'new_value'}); + await expect(redoBtn).toBeDisabled(); + }); + + datalensTest('Undo reverts field visibility toggle', async ({page}) => { + // Disable "Show hidden" so we cant observe the hidden field + const settingsBtn = page.locator(slct(DatasetFieldsTabQa.TableSettingsBtn)); + await settingsBtn.click(); + const showHiddenMenuItem = await page.waitForSelector( + slct(DatasetEditorTableSettingsItems.ShowHidden), + ); + await showHiddenMenuItem.click(); + + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + + // Hide first field + const hiddenBtn = page.locator(slct(DatasetFieldsTabQa.FieldVisibleColumnIcon)).first(); + await hiddenBtn.click(); + + const rowsCountAfterHide = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterHide).toBe(rowsCountBefore - 1); + + // Undo the hide + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const rowsCountAfterUndo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterUndo).toBe(rowsCountBefore); + }); + + datalensTest('Redo restores field visibility toggle', async ({page}) => { + // Get the visibility state of the first field before hiding + const firstFieldVisibleIcon = page + .locator(slct(DatasetFieldsTabQa.FieldVisibleColumnIcon)) + .first(); + const visibleClassBefore = await firstFieldVisibleIcon.getAttribute('title'); + + // Hide first field + await firstFieldVisibleIcon.click(); + + const visibleClassAfterHide = await firstFieldVisibleIcon.getAttribute('title'); + expect(visibleClassAfterHide).not.toBe(visibleClassBefore); + + // Undo the hide + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + // Redo the hide + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const visibleClassAfterRedo = await firstFieldVisibleIcon.getAttribute('title'); + expect(visibleClassAfterRedo).toBe(visibleClassAfterHide); + }); + + datalensTest('Undo and Redo restores field duplication', async ({page}) => { + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + await fieldInput.hover(); + // Duplicate field via context menu + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const duplicateItem = page.locator(slct(DatasetFieldContextMenuItemsQA.DUPLICATE)); + const duplicatePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await duplicateItem.click(); + await duplicatePromise; + + const rowsCountAfterDuplicate = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterDuplicate).toBe(rowsCountBefore + 1); + + // Undo the duplication + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const rowsCountAfterUndo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterUndo).toBe(rowsCountBefore); + + // Redo the duplication + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const rowsCountAfterRedo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterRedo).toBe(rowsCountBefore + 1); + }); + + datalensTest('Undo and Redo restores field removal', async ({page}) => { + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + await fieldInput.hover(); + // Remove field via context menu + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const removeItem = page.locator(slct(DatasetFieldContextMenuItemsQA.REMOVE)); + const removePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await removeItem.click(); + await removePromise; + + const rowsCountAfterRemove = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterRemove).toBe(rowsCountBefore - 1); + + // Undo the removal + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const rowsCountAfterUndo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterUndo).toBe(rowsCountBefore); + + // Redo the removal + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const rowsCountAfterRedo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterRedo).toBe(rowsCountBefore - 1); + }); + + datalensTest('Undo reverts field description change', async ({page}) => { + const descriptionInput = page + .locator(slct(DatasetFieldsTabQa.FieldDescriptionColumnInput)) + .first() + .locator('input'); + const originalValue = await descriptionInput.inputValue(); + + await descriptionInput.fill('test description for history'); + await page.keyboard.press('Enter'); + + const updatedValue = await descriptionInput.inputValue(); + expect(updatedValue).toBe('test description for history'); + + // Undo the description change + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const restoredValue = await descriptionInput.inputValue(); + expect(restoredValue).toBe(originalValue); + }); + + datalensTest('Redo restores field description change', async ({page}) => { + const descriptionInput = page + .locator(slct(DatasetFieldsTabQa.FieldDescriptionColumnInput)) + .first() + .locator('input'); + const originalValue = await descriptionInput.inputValue(); + const newDescription = 'test description for redo'; + + await descriptionInput.fill(newDescription); + await page.keyboard.press('Enter'); + + // Undo + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const afterUndo = await descriptionInput.inputValue(); + expect(afterUndo).toBe(originalValue); + + // Redo + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const afterRedo = await descriptionInput.inputValue(); + expect(afterRedo).toBe(newDescription); + }); + + datalensTest('Undo reverts field type change', async ({page}) => { + const typeSelect = page.locator(slct(DatasetFieldsTabQa.FieldTypeColumnBtn)); + const { + targetOption, + selectBtn: typeSelectBtn, + originalSelectText: originalTypeText, + } = await datasetPage.datasetFieldsTable.changeFieldSelect(typeSelect); + + if (!targetOption || !typeSelectBtn) { + return; + } + + const validatePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await targetOption.click(); + await validatePromise; + + const changedTypeText = await typeSelectBtn.textContent(); + expect(changedTypeText).not.toBe(originalTypeText); + + // Undo the type change + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const restoredTypeText = await typeSelectBtn.textContent(); + expect(restoredTypeText).toBe(originalTypeText); + }); + + datalensTest('Redo restores field type change', async ({page}) => { + const typeSelect = page.locator(slct(DatasetFieldsTabQa.FieldTypeColumnBtn)); + const { + targetOption, + targetOptionText, + originalSelectText: originalTypeText, + selectBtn: typeSelectBtn, + } = await datasetPage.datasetFieldsTable.changeFieldSelect(typeSelect); + if (!targetOption || !typeSelectBtn) { + return; + } + + const validatePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await targetOption.click(); + await validatePromise; + + // Undo + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const afterUndo = await typeSelectBtn.textContent(); + expect(afterUndo).toBe(originalTypeText); + + // Redo + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const afterRedo = await typeSelectBtn.textContent(); + expect(afterRedo).toBe(targetOptionText); + }); + + datalensTest('Undo reverts field aggregation change', async ({page}) => { + const aggregationSelectBtns = page.locator( + slct(DatasetFieldsTabQa.FieldAggregationColumnBtn), + ); + const { + targetOption, + selectBtn: aggregationSelectBtn, + originalSelectText: originalAggregationText, + } = await datasetPage.datasetFieldsTable.changeFieldSelect(aggregationSelectBtns); + + if (!targetOption || !aggregationSelectBtn) { + return; + } + + const validatePromise = getValidatePromise(page); + await targetOption.click(); + await validatePromise; + + const changedAggregationText = await aggregationSelectBtn.textContent(); + expect(changedAggregationText).not.toBe(originalAggregationText); + + // Undo + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const restoredAggregationText = await aggregationSelectBtn.textContent(); + expect(restoredAggregationText).toBe(originalAggregationText); + }); + + datalensTest('Redo restores field aggregation change', async ({page}) => { + const aggregationSelectBtns = page.locator( + slct(DatasetFieldsTabQa.FieldAggregationColumnBtn), + ); + const { + targetOption, + targetOptionText, + originalSelectText: originalAggregationText, + selectBtn: aggregationSelectBtn, + } = await datasetPage.datasetFieldsTable.changeFieldSelect(aggregationSelectBtns); + + if (!targetOption || !aggregationSelectBtn) { + return; + } + + const validatePromise = getValidatePromise(page); + await targetOption.click(); + await validatePromise; + + // Undo + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const afterUndo = await aggregationSelectBtn.textContent(); + expect(afterUndo).toBe(originalAggregationText); + + // Redo + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const afterRedo = await aggregationSelectBtn.textContent(); + expect(afterRedo).toBe(targetOptionText); + }); + + datalensTest('Undo and redo restore field edit via field editor dialog', async ({page}) => { + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const originalName = await fieldInput.inputValue(); + await fieldInput.hover(); + + // Open field editor via context menu + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const editItem = page.locator(slct(DatasetFieldContextMenuItemsQA.EDIT)); + await editItem.click(); + + const fieldEditorDialog = page.locator(slct(FieldEditorQa.Dialog)); + await expect(fieldEditorDialog).toBeVisible(); + const fieldEditor = new FieldEditor(page); + const changedName = `${originalName}_edited`; + await fieldEditor.changeName(changedName); + + const applyPromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await fieldEditor.clickToApplyButton(); + await applyPromise; + + // Undo + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const afterUndo = await fieldInput.inputValue(); + expect(afterUndo).toBe(originalName); + + // Redo + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const afterRedo = await fieldInput.inputValue(); + expect(afterRedo).toBe(changedName); + }); + + datalensTest('Undo reverts batch deletion of selected fields', async ({page}) => { + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + await datasetPage.datasetFieldsTable.selectTwoCheckboxes(); + + // Click batch delete button + const actionsPanel = page.locator(slct(DatasetFieldsTabQa.BatchActionsPanel)); + await expect(actionsPanel).toBeVisible(); + + const deleteBtn = actionsPanel.locator(slct(DatasetFieldTabBatchPanelQa.BatchDelete)); + await deleteBtn.click(); + const applyDelete = await page.waitForSelector(slct(DialogConfirmQA.ApplyButton)); + const deletePromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await applyDelete.click(); + await deletePromise; + + const rowsCountAfterDelete = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterDelete).toBe(rowsCountBefore - 2); + + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const rowsCountAfterUndo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterUndo).toBe(rowsCountBefore); + + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const rowsCountAfterRedo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterRedo).toBe(rowsCountBefore - 2); + }); + + datalensTest('Undo reverts batch hide of selected fields', async ({page}) => { + const settingsBtn = page.locator(slct(DatasetFieldsTabQa.TableSettingsBtn)); + await settingsBtn.click(); + const showHiddenMenuItem = await page.waitForSelector( + slct(DatasetEditorTableSettingsItems.ShowHidden), + ); + await showHiddenMenuItem.click(); + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + await datasetPage.datasetFieldsTable.selectTwoCheckboxes(); + + const actionsPanel = page.locator(slct(DatasetFieldsTabQa.BatchActionsPanel)); + await expect(actionsPanel).toBeVisible(); + + const hideBtn = actionsPanel.locator('button').nth(1); + await hideBtn.click(); + const applyHide = await page.waitForSelector(slct(DialogConfirmQA.ApplyButton)); + await applyHide.click(); + + // After hiding, the hidden fields should disappear (Show hidden is off by default) + const rowsCountAfterHide = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterHide).toBe(rowsCountBefore - 2); + + // Undo the batch hide + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const rowsCountAfterUndo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterUndo).toBe(rowsCountBefore); + + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const rowsCountAfterRedo = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterRedo).toBe(rowsCountBefore - 2); + }); + + datalensTest('Multiple redo steps restore multiple changes', async ({page}) => { + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const step1Value = 'step1'; + const step2Value = 'step2'; + + // Two changes + const {originalValue} = (await datasetPage.renameFirstField({value: step1Value})) ?? {}; + await datasetPage.renameFirstField({value: step2Value}); + + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const afterFirstUndo = await fieldInput.inputValue(); + expect(afterFirstUndo).toBe(step1Value); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const afterUndoAll = await fieldInput.inputValue(); + expect(afterUndoAll).toBe(originalValue); + + // Redo first + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const afterFirstRedo = await fieldInput.inputValue(); + expect(afterFirstRedo).toBe(step1Value); + + // Redo second + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + + const afterSecondRedo = await fieldInput.inputValue(); + expect(afterSecondRedo).toBe(step2Value); + }); + + datalensTest('Undo button becomes disabled at the beginning of history', async ({page}) => { + await datasetPage.renameFirstField(); + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + await expect(undoBtn).toBeDisabled(); + }); + + datalensTest('Redo button becomes disabled at the end of history', async ({page}) => { + await datasetPage.renameFirstField(); + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + await redoBtn.click(); + await expect(redoBtn).toBeDisabled(); + }); + + datalensTest('Keyboard shortcut Cmd/Ctrl+Z triggers undo', async ({page}) => { + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const {originalValue} = (await datasetPage.renameFirstField()) ?? {}; + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + // Click outside the input to unfocus before using hotkey + await page.locator(slct(DatasetFieldsTabQa.DatasetEditor)).click(); + await page.keyboard.press(`ControlOrMeta+z`); + + const restoredValue = await fieldInput.inputValue(); + expect(restoredValue).toBe(originalValue); + }); + + datalensTest('Keyboard shortcut Cmd/Ctrl+Shift+Z triggers redo', async ({page}) => { + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + const {newValue: changedValue} = (await datasetPage.renameFirstField()) ?? {}; + + // Undo via button + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const redoBtn = page.locator(slct(EditHistoryQA.RedoBtn)); + await expect(redoBtn).toBeEnabled(); + // Click outside the input to unfocus before using hotkey + await page.locator(slct(DatasetFieldsTabQa.DatasetEditor)).click(); + await page.keyboard.press(`ControlOrMeta+Shift+z`); + + const restoredValue = await fieldInput.inputValue(); + expect(restoredValue).toBe(changedValue); + }); + + datalensTest('Undo reverts mixed action types in correct order', async ({page}) => { + const rowsCountBefore = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + + const fieldInput = datasetPage.datasetFieldsTable.getFieldNameInput(); + + // // Step 1: Rename a field + const {newValue, originalValue} = (await datasetPage.renameFirstField()) ?? {}; + + // Step 2: Duplicate a field via context menu + await fieldInput.hover(); + const contextMenuBtn = page.locator(slct(DatasetFieldsTabQa.FieldContextMenuBtn)).first(); + await contextMenuBtn.click(); + + const duplicateItem = page.locator(slct(DatasetFieldContextMenuItemsQA.DUPLICATE)); + const dupPromise = datasetPage.waitForSuccessfulResponse(VALIDATE_DATASET_URL); + await duplicateItem.click(); + await dupPromise; + + const rowsCountAfterDup = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterDup).toBe(rowsCountBefore + 1); + + const undoBtn = page.locator(slct(EditHistoryQA.UndoBtn)); + + // Undo step 2 (duplicate) + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + + const rowsCountAfterUndoDup = await page + .locator(slct(DatasetFieldsTabQa.FieldNameColumnInput)) + .count(); + expect(rowsCountAfterUndoDup).toBe(rowsCountBefore); + const afterUndoFirst = await fieldInput.inputValue(); + expect(afterUndoFirst).toBe(newValue); + + // Undo step 1 (rename) + await expect(undoBtn).toBeEnabled(); + await undoBtn.click(); + const afterUndoAll = await fieldInput.inputValue(); + expect(afterUndoAll).toBe(originalValue); + }); +}); diff --git a/tests/suites/dataset/relationsWithValidationError.test.ts b/tests/suites/dataset/relationsWithValidationError.test.ts index 606dce6cce..b8042b8c6c 100644 --- a/tests/suites/dataset/relationsWithValidationError.test.ts +++ b/tests/suites/dataset/relationsWithValidationError.test.ts @@ -2,7 +2,7 @@ import {Page} from '@playwright/test'; import DatasetPage from '../../page-objects/dataset/DatasetPage'; import datalensTest from '../../utils/playwright/globalTestDefinition'; -import {DatasetActionQA, DatasetDialogRelationQA} from '../../../src/shared'; +import {DATASET_TAB, DatasetActionQA, DatasetDialogRelationQA} from '../../../src/shared'; import {openTestPage, slct} from '../../utils'; import {RobotChartsDatasetUrls} from '../../utils/constants'; @@ -25,7 +25,7 @@ datalensTest.describe('Datasets - validation error does not break the communicat const datasetPage = new DatasetPage({page}); await openTestPage(page, RobotChartsDatasetUrls.DatasetWithValidationError); - await datasetPage.openSourcesPanel(); + await datasetPage.openTab(DATASET_TAB.SOURCES); // waiting for the validation error to appear await datasetPage.waitForSelector(SELECTORS.TOAST_VALIDATION_ERROR); diff --git a/tests/suites/dataset/sourceEditorDialog.test.ts b/tests/suites/dataset/sourceEditorDialog.test.ts index be9f876e4a..19d2636592 100644 --- a/tests/suites/dataset/sourceEditorDialog.test.ts +++ b/tests/suites/dataset/sourceEditorDialog.test.ts @@ -4,7 +4,7 @@ import {CommonSelectors} from '../../page-objects/constants/common-selectors'; import {openTestPage, slct} from '../../utils'; import datalensTest from '../../utils/playwright/globalTestDefinition'; import {RobotChartsDatasetUrls} from '../../utils/constants'; -import {DatasetSourcesTableQa} from '../../../src/shared'; +import {DatasetSourceEditorDialogQA, DatasetSourcesTableQa} from '../../../src/shared'; const checkSourceType = async (page: Page, expectedSourceType: string) => { const promise = page.waitForRequest(/validateDataset/, { @@ -29,22 +29,30 @@ datalensTest.describe('Datasets - source editor dialog', () => { datalensTest('Saving name of editing source', async ({page}: {page: Page}) => { const ytPath = YT_PATH; - const inputLocator = page.locator(`${slct('source-editor-path')} input`); + const inputLocator = page.locator( + `${slct(DatasetSourceEditorDialogQA.EditPathInput)} input`, + ); await inputLocator.fill(ytPath); await page.click('.g-dialog-footer__button_action_apply'); await page.click(`${slct(DatasetSourcesTableQa.Source)} .g-button`); await page.click(`${slct(DatasetSourcesTableQa.SourceMenu)} .g-menu__list-item`); - const titleInputLocator = page.locator(`${slct('source-editor-title')} input`); + const titleInputLocator = page.locator( + `${slct(DatasetSourceEditorDialogQA.EditTitleInput)} input`, + ); await expect(titleInputLocator).toHaveValue('bi_2132_one_col_sorting'); }); datalensTest('Possible to save same name of existing source', async ({page}: {page: Page}) => { const ytPath = YT_PATH; - const inputLocator = page.locator(`${slct('source-editor-path')} input`); - const titleInputLocator = page.locator(`${slct('source-editor-title')} input`); + const inputLocator = page.locator( + `${slct(DatasetSourceEditorDialogQA.EditPathInput)} input`, + ); + const titleInputLocator = page.locator( + `${slct(DatasetSourceEditorDialogQA.EditTitleInput)} input`, + ); await inputLocator.fill(ytPath); await titleInputLocator.fill('test name'); @@ -53,7 +61,7 @@ datalensTest.describe('Datasets - source editor dialog', () => { await page.click(`${slct(DatasetSourcesTableQa.Source)} .g-button`); await page.click(`${slct(DatasetSourcesTableQa.SourceMenu)} .g-menu__list-item`); - await page.waitForSelector(slct('source-editor-title')); + await page.waitForSelector(slct(DatasetSourceEditorDialogQA.EditTitleInput)); // return same name to title await titleInputLocator.fill('new name'); @@ -63,12 +71,14 @@ datalensTest.describe('Datasets - source editor dialog', () => { await page.waitForTimeout(1000); - const error = page.locator(slct('source-editor-dialog')); + const error = page.locator(slct(DatasetSourceEditorDialogQA.Dialog)); await expect(error).toHaveCount(0); }); datalensTest('Possible to change source name', async ({page}: {page: Page}) => { - const titleInputLocator = page.locator(`${slct('source-editor-title')} input`); + const titleInputLocator = page.locator( + `${slct(DatasetSourceEditorDialogQA.EditTitleInput)} input`, + ); await titleInputLocator.fill('Example1'); await expect(titleInputLocator).toHaveValue('Example1'); @@ -81,13 +91,19 @@ datalensTest.describe('Datasets - source editor dialog', () => { const inputLocator = page.locator(inputSelector).first(); const tableTab = page - .locator(`${slct('datasets-source-switcher')} .g-segmented-radio-group__option`) + .locator( + `${slct(DatasetSourceEditorDialogQA.SourceEditorSwitch)} .g-segmented-radio-group__option`, + ) .nth(0); const listTab = page - .locator(`${slct('datasets-source-switcher')} .g-segmented-radio-group__option`) + .locator( + `${slct(DatasetSourceEditorDialogQA.SourceEditorSwitch)} .g-segmented-radio-group__option`, + ) .nth(1); const rangeTab = page - .locator(`${slct('datasets-source-switcher')} .g-segmented-radio-group__option`) + .locator( + `${slct(DatasetSourceEditorDialogQA.SourceEditorSwitch)} .g-segmented-radio-group__option`, + ) .nth(2); await tableTab.check(); diff --git a/tests/utils/constants.ts b/tests/utils/constants.ts index 6a1172342d..6c1ac63825 100644 --- a/tests/utils/constants.ts +++ b/tests/utils/constants.ts @@ -163,6 +163,7 @@ export const enum RobotChartsDatasetUrls { DatasetWithDeletedConnection = '/datasets/rupa8kcmvnmil', DatasetWithValidationError = '/datasets/mqjnuhmltfs8c', DatasetSourcesListing = '/datasets/1ui7gozkkugal', + DatasetWithCsvConnection = '/datasets/gd1gjlpr47gc0', } export const mapDatasetToDatasetPath = {