From 20a69e6d273f489db95bcb2cc4328d5b7a7b3d81 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 23 Jan 2026 14:57:29 +0200 Subject: [PATCH 1/2] Add test --- .../__mock__/model/cell/data_cell.ts | 8 ++ .../__tests__/__mock__/model/grid_core.ts | 5 + .../__tests__/validating.integration.test.ts | 123 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts index 16488785a4be..d124f6f8870a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts @@ -1,6 +1,9 @@ +import { TextBoxModel } from '@ts/ui/__tests__/__mock__/model/textbox'; + const SELECTORS = { editCell: 'dx-editor-cell', invalidCell: 'invalid', + textBox: 'dx-textbox', }; export class DataCellModel { @@ -27,4 +30,9 @@ export class DataCellModel { public getHTML(): string { return this.root?.innerHTML ?? ''; } + + public getEditor(): TextBoxModel { + const editorElement = this.root?.querySelector(`.${SELECTORS.textBox}`) as HTMLElement; + return new TextBoxModel(editorElement); + } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts index 0d93490374ae..cccec361732f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts @@ -25,6 +25,7 @@ const SELECTORS = { editForm: 'edit-form', headerCellIndicators: 'dx-column-indicators', headerCellFilter: 'dx-header-filter', + revertButton: 'dx-revert-button', }; export abstract class GridCoreModel { @@ -101,6 +102,10 @@ export abstract class GridCoreModel { return new ToastModel(this.getToastContainer()); } + public getRevertButton(): HTMLElement { + return document.body.querySelector(`.${SELECTORS.revertButton}`) as HTMLElement; + } + public addWidgetPrefix(classNames: string): string { const componentName = this.NAME; diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts new file mode 100644 index 000000000000..2b91d74b7c52 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts @@ -0,0 +1,123 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import DataGrid from '@js/ui/data_grid'; +import errors from '@js/ui/widget/ui.errors'; +import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; + +const SELECTORS = { + gridContainer: '#gridContainer', +}; + +const GRID_CONTAINER_ID = 'gridContainer'; + +const createDataGrid = async ( + options: DataGridProperties = {}, +): Promise<{ + $container: dxElementWrapper; + component: DataGridModel; + instance: DataGrid; +}> => new Promise((resolve) => { + const $container = $('
') + .attr('id', GRID_CONTAINER_ID) + .appendTo(document.body); + + const dataGridOptions: DataGridProperties = { + keyExpr: 'id', + ...options, + }; + + const instance = new DataGrid($container.get(0) as HTMLDivElement, dataGridOptions); + const component = new DataGridModel($container.get(0) as HTMLElement); + + jest.runAllTimers(); + + resolve({ + $container, + component, + instance, + }); +}); + +const beforeTest = (): void => { + jest.useFakeTimers(); + jest.spyOn(errors, 'log').mockImplementation(jest.fn()); + jest.spyOn(errors, 'Error').mockImplementation(() => ({})); +}; + +const afterTest = (): void => { + const $container = $(SELECTORS.gridContainer); + const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid; + + dataGrid.dispose(); + $container.remove(); + jest.clearAllMocks(); + jest.useRealTimers(); +}; + +describe('Bugs', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('T1308327 - DataGrid - Cell value is not restored after canceling changes in cell editing mode if repaintChangesOnly is enabled', () => { + it('should restore cell value after canceling changes with validation error', async () => { + const data = [ + { id: 1, name: 'Job 1', article: 'Article A' }, + { id: 2, name: 'Job 2', article: 'Article B' }, + ]; + + const { instance, component } = await createDataGrid({ + dataSource: data, + keyExpr: 'id', + repaintChangesOnly: true, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [ + { + dataField: 'name', + showEditorAlways: true, + validationRules: [ + { type: 'required', message: 'Required field' }, + ], + }, + { + dataField: 'article', + }, + ], + }); + + const firstCell = component.getDataCell(0, 0); + const firstEditor = firstCell.getEditor(); + + firstEditor.setValue(''); + jest.runAllTimers(); + + expect(component.getDataCell(0, 0).isValidCell).toBe(false); + + component.getRevertButton().click(); + jest.runAllTimers(); + + expect(instance.cellValue(0, 'name')).toBe('Job 1'); + expect(component.getDataCell(0, 0).isValidCell).toBe(true); + + const secondCell = component.getDataCell(1, 0); + const secondEditor = secondCell.getEditor(); + + secondEditor.setValue(''); + jest.runAllTimers(); + + expect(component.getDataCell(1, 0).isValidCell).toBe(false); + + component.getRevertButton().click(); + jest.runAllTimers(); + + expect(instance.cellValue(1, 'name')).toBe('Job 2'); + expect(component.getDataCell(1, 0).isValidCell).toBe(true); + }); + }); +}); From 6c9703aa831b7b862597d95f88d468dcce3a1b5e Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Sat, 24 Jan 2026 02:03:35 +0400 Subject: [PATCH 2/2] Keep a validation state if repaintChangesOnly is switched on to let the revert button get access to initial values via _getOldData() --- .../__internal/grids/grid_core/validating/m_validating.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index 5c81455478b3..936fd9768781 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -979,7 +979,11 @@ export const validatingEditingExtender = (Base: ModuleType) = } protected _beforeCancelEditData() { - this._validatingController.initValidationState(); + // Don't reset a validation state if repaintChangesOnly is switched on + // to let the revert button get access to initial values via _getOldData() + if (!this.option('repaintChangesOnly')) { + this._validatingController.initValidationState(); + } super._beforeCancelEditData(); }