From d1f910144bf316840081e27cb224b9093ef16e61 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 23 Jan 2026 14:57:29 +0200 Subject: [PATCH 1/6] 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 7001075aec476bbeccbd3930ec27bf7e1ef919d4 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Sat, 24 Jan 2026 02:03:35 +0400 Subject: [PATCH 2/6] 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(); } From d1b7be07cf7552489162fd86866a7081e5dfb06a Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Thu, 29 Jan 2026 18:29:53 +0400 Subject: [PATCH 3/6] reset validation state if batch edit mode enabled --- .../grids/grid_core/editing/m_editing.ts | 5 ++ .../__tests__/validating.integration.test.ts | 61 ++----------------- .../grid_core/validating/m_validating.ts | 5 +- 3 files changed, 14 insertions(+), 57 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts index b19c278be7fa..be0759e58a53 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts @@ -51,6 +51,7 @@ import { EDIT_FORM_CLASS, EDIT_ICON_CLASS, EDIT_LINK_CLASS, + EDIT_MODE_BATCH, EDIT_MODE_POPUP, EDIT_MODE_ROW, EDIT_MODES, @@ -216,6 +217,10 @@ class EditingControllerImpl extends modules.ViewController { return CELL_BASED_MODES.includes(editMode); } + protected isBatchBasedEditMode(): boolean { + return this.getEditMode() === EDIT_MODE_BATCH; + } + private _getDefaultEditorTemplate() { return (container, options) => { const $editor = $('
').appendTo(container); 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 index 2b91d74b7c52..1742301fe5f4 100644 --- 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 @@ -1,62 +1,11 @@ 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(); -}; +import { + afterTest, + beforeTest, + createDataGrid, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; describe('Bugs', () => { beforeEach(beforeTest); 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 936fd9768781..2962fccf7e68 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,9 +979,12 @@ export const validatingEditingExtender = (Base: ModuleType) = } protected _beforeCancelEditData() { + const isBatchBasedEditMode = this.isBatchBasedEditMode(); + const repaintChangesOnly = this.option('repaintChangesOnly'); + // 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')) { + if (isBatchBasedEditMode || !repaintChangesOnly) { this._validatingController.initValidationState(); } From fe24272b1ecb509e77cc38ecfd987b7b9e49a516 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Thu, 29 Jan 2026 19:16:26 +0400 Subject: [PATCH 4/6] reset validation state if batch cell mode enabled --- .../grids/grid_core/editing/m_editing.ts | 5 --- .../grid_core/validating/m_validating.ts | 31 +++++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts index be0759e58a53..b19c278be7fa 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts @@ -51,7 +51,6 @@ import { EDIT_FORM_CLASS, EDIT_ICON_CLASS, EDIT_LINK_CLASS, - EDIT_MODE_BATCH, EDIT_MODE_POPUP, EDIT_MODE_ROW, EDIT_MODES, @@ -217,10 +216,6 @@ class EditingControllerImpl extends modules.ViewController { return CELL_BASED_MODES.includes(editMode); } - protected isBatchBasedEditMode(): boolean { - return this.getEditMode() === EDIT_MODE_BATCH; - } - private _getDefaultEditorTemplate() { return (container, options) => { const $editor = $('
').appendTo(container); 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 2962fccf7e68..58d834edb8f4 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 @@ -936,14 +936,7 @@ export const validatingEditingExtender = (Base: ModuleType) = }); this._focusEditingCell(); } else if (!cancel) { - let shouldResetValidationState = true; - - if (isCellEditMode) { - const columns = this._columnsController.getColumns(); - const columnsWithValidatingEditors = columns.filter((col) => col.showEditorAlways && col.validationRules?.length > 0).length > 0; - - shouldResetValidationState = !columnsWithValidatingEditors; - } + const shouldResetValidationState = this._shouldResetValidationState(); if (shouldResetValidationState) { this._validatingController.initValidationState(); @@ -979,18 +972,30 @@ export const validatingEditingExtender = (Base: ModuleType) = } protected _beforeCancelEditData() { - const isBatchBasedEditMode = this.isBatchBasedEditMode(); - const repaintChangesOnly = this.option('repaintChangesOnly'); + const shouldResetValidationState = this._shouldResetValidationState(); - // Don't reset a validation state if repaintChangesOnly is switched on - // to let the revert button get access to initial values via _getOldData() - if (isBatchBasedEditMode || !repaintChangesOnly) { + if (shouldResetValidationState) { this._validatingController.initValidationState(); } super._beforeCancelEditData(); } + private _shouldResetValidationState(): boolean { + const isCellEditMode = this.getEditMode() === EDIT_MODE_CELL; + + if (isCellEditMode) { + const columns = this._columnsController.getColumns(); + const columnsWithValidatingEditors = columns.filter( + (col) => col.showEditorAlways && col.validationRules?.length > 0, + ); + + return columnsWithValidatingEditors.length === 0; + } + + return true; + } + private _showErrorRow(change) { let $popupContent; const items = this._dataController.items(); From 24dd281738cd2a8efa106b60c4c26bb682b21f58 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 30 Jan 2026 10:42:06 +0400 Subject: [PATCH 5/6] reset validation state only for changes in cell edit mode --- .../grid_core/validating/m_validating.ts | 34 ++++++++++++++++++- .../editing.integration.tests.js | 3 +- 2 files changed, 34 insertions(+), 3 deletions(-) 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 58d834edb8f4..a8e5ac0d4485 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 @@ -115,6 +115,34 @@ export class ValidatingController extends modules.Controller { this._validationStateCache = {}; } + public resetValidationStateForChanges(changes) { + if (!changes?.length) { + return; + } + + changes.forEach(({ key }) => { + this._removeValidationData(key); + }); + } + + private _removeValidationData(key) { + if (!this._validationState?.length) { + return; + } + + const keyHash = getKeyHash(key); + const isObjectKeyHash = isObject(keyHash); + + if (!isObjectKeyHash && this._validationStateCache) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this._validationStateCache[keyHash]; + } + + this._validationState = this._validationState.filter((data) => ( + isObjectKeyHash ? !equalByValue(data.key, key) : data.key !== key + )); + } + public _rowIsValidated(change) { const validationData = this._getValidationData(change?.key); @@ -972,10 +1000,14 @@ export const validatingEditingExtender = (Base: ModuleType) = } protected _beforeCancelEditData() { + const validatingController = this._validatingController; const shouldResetValidationState = this._shouldResetValidationState(); if (shouldResetValidationState) { - this._validatingController.initValidationState(); + validatingController.initValidationState(); + } else { + const changes = this.getChanges(); + validatingController.resetValidationStateForChanges(changes); } super._beforeCancelEditData(); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js index de02e485d012..154120751d7b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js @@ -2809,10 +2809,9 @@ QUnit.module('Editing', baseModuleConfig, () => { this.clock.tick(10); $(grid.getCellElement(0, 1)).trigger('dxclick'); this.clock.tick(10); - const callCount = action === 'close edit cell' ? 3 : 4; // assert - assert.equal(validationCallback.callCount, callCount, 'validation callback call count'); + assert.equal(validationCallback.callCount, 3, 'validation callback call count'); }); }); From 08a41f70a7be4898bf6342178ebee6c054ff35f8 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Mon, 2 Feb 2026 10:19:28 +0400 Subject: [PATCH 6/6] fix test naming --- .../validating/__tests__/validating.integration.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 1742301fe5f4..c4200ccd0795 100644 --- 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 @@ -7,12 +7,13 @@ import { createDataGrid, } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; -describe('Bugs', () => { +describe('DataGrid Cell Editing', () => { 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 () => { + // T1308327 + describe('when showEditorAlways and repaintChangesOnly is enabled', () => { + it('should restore the value after canceling changes with validation error (T1308327)', async () => { const data = [ { id: 1, name: 'Job 1', article: 'Article A' }, { id: 2, name: 'Job 2', article: 'Article B' },