diff --git a/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_controller.ts new file mode 100644 index 000000000000..c978b138692e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_controller.ts @@ -0,0 +1,20 @@ +import errors from '@js/ui/widget/ui.errors'; +import dataSourceAdapterProvider from '@ts/grids/data_grid/m_data_source_adapter'; +import { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; +import type { DataSourceAdapterProvider } from '@ts/grids/grid_core/data_source_adapter/types'; + +export class DataGridDataSourceController extends DataSourceController { + protected getAdapterProvider(): DataSourceAdapterProvider { + return dataSourceAdapterProvider; + } + + protected getSpecificDataSourceOption(): unknown { + const dataSource = this.option('dataSource'); + + if (dataSource && !Array.isArray(dataSource) && this.option('keyExpr')) { + errors.log('W1011'); + } + + return super.getSpecificDataSourceOption(); + } +} diff --git a/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_module.ts b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_module.ts new file mode 100644 index 000000000000..87d5621798c0 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_module.ts @@ -0,0 +1,9 @@ +import gridCore from '@ts/grids/data_grid/m_core'; + +import { DataGridDataSourceController } from './data_source_controller'; + +gridCore.registerModule('dataSource', { + controllers: { + dataSource: DataGridDataSourceController, + }, +}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_data_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/m_data_controller.ts deleted file mode 100644 index e6632d7df28f..000000000000 --- a/packages/devextreme/js/__internal/grids/data_grid/m_data_controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import errors from '@js/ui/widget/ui.errors'; -import { DataController, dataControllerModule } from '@ts/grids/grid_core/data_controller/data_controller'; - -import type { DataSourceAdapterProvider } from '../grid_core/data_source_adapter/types'; -import gridCore from './m_core'; -import dataSourceAdapterProvider from './m_data_source_adapter'; - -class DataGridDataController extends DataController { - protected _getDataSourceAdapterProvider(): DataSourceAdapterProvider { - return dataSourceAdapterProvider; - } - - protected _getSpecificDataSourceOption() { - const dataSource = this.option('dataSource'); - - if (dataSource && !Array.isArray(dataSource) && this.option('keyExpr')) { - errors.log('W1011'); - } - - return super._getSpecificDataSourceOption(); - } -} - -export { DataGridDataController as DataController }; - -gridCore.registerModule('data', { - defaultOptions: dataControllerModule.defaultOptions, - controllers: { - data: DataGridDataController, - }, -}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts index 1710af383abd..546d0b54c089 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts @@ -1,6 +1,7 @@ import './module_not_extended/column_headers'; import './m_columns_controller'; -import './m_data_controller'; +import './data_source/data_source_module'; +import './module_not_extended/data_controller'; import './module_not_extended/sorting'; import './module_not_extended/rows'; import './module_not_extended/context_menu'; @@ -24,6 +25,7 @@ import gridCore from './m_core'; const DATAGRID_DEPRECATED_TEMPLATE_WARNING = 'Specifying grid templates with the jQuery selector name is now deprecated. Use the DOM Node or the jQuery object that references this selector instead.'; gridCore.registerModulesOrder([ + 'dataSource', 'stateStoring', 'columns', 'selection', diff --git a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/data_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/data_controller.ts new file mode 100644 index 000000000000..5965240a4e70 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/data_controller.ts @@ -0,0 +1,5 @@ +import { dataControllerModule } from '@ts/grids/grid_core/data_controller/data_controller'; + +import gridCore from '../m_core'; + +gridCore.registerModule('data', dataControllerModule); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.data_source.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.data_source.test.ts deleted file mode 100644 index 0d64fec2e747..000000000000 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.data_source.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - afterEach, - beforeEach, - describe, - expect, - it, -} from '@jest/globals'; -import DataSource from '@js/data/data_source'; -import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; -import { - afterTest, - beforeTest, - createDataGrid, - flushAsync, -} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; - -import { DataController } from '../data_controller'; - -declare class ExposedDataController extends DataController { - public isSharedDataSource?: boolean; -} - -const DATA = [ - { id: 1, value: 'a' }, - { id: 2, value: 'b' }, -]; - -const getIsSharedDataSource = (instance: DataGridInstance): boolean | undefined => { - const dataController = instance.getController('data') as unknown as ExposedDataController; - - return dataController.isSharedDataSource; -}; - -describe('DataController data source', () => { - beforeEach(beforeTest); - afterEach(afterTest); - - describe('isSharedDataSource', () => { - it('should be true when a DataSource instance is passed', async () => { - const sharedDataSource = new DataSource({ store: DATA, key: 'id' }); - - const { instance } = await createDataGrid({ dataSource: sharedDataSource }); - - expect(getIsSharedDataSource(instance)).toBe(true); - }); - - it('should be false when a plain array is passed', async () => { - const { instance } = await createDataGrid({ dataSource: DATA }); - - expect(getIsSharedDataSource(instance)).toBe(false); - }); - - it('should reset to false after switching from a shared DataSource to a plain array', async () => { - const sharedDataSource = new DataSource({ store: DATA, key: 'id' }); - - const { instance } = await createDataGrid({ dataSource: sharedDataSource }); - expect(getIsSharedDataSource(instance)).toBe(true); - - instance.option('dataSource', DATA); - await flushAsync(); - - expect(getIsSharedDataSource(instance)).toBe(false); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts index 5b58e6902297..439b28289ea6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts @@ -1,11 +1,8 @@ -import { DataSource as DataSourceClass } from '@js/common/data/data_source/data_source'; -import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import type { Callback } from '@js/core/utils/callbacks'; import { deferRender } from '@js/core/utils/common'; import { logger } from '@js/core/utils/console'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; -import { extend } from '@js/core/utils/extend'; import { isDefined } from '@js/core/utils/type'; import type { StoreChange } from '@js/data/store'; import errors from '@js/ui/widget/ui.errors'; @@ -15,9 +12,10 @@ import type Store from '@ts/data/abstract_store'; import type { DataSource } from '@ts/data/data_source/data_source'; import type { ChangingEvent } from '@ts/data/data_source/types'; import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_controller/types'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; import type { - ChangedEvent, DataSourceAdapterProvider, LoadOperation, OperationTypes, RawItemData, + ChangedEvent, LoadOperation, OperationTypes, RawItemData, } from '@ts/grids/grid_core/data_source_adapter/types'; import { isLocalStore } from '@ts/grids/grid_core/data_source_adapter/utils/store'; import modules from '@ts/grids/grid_core/m_modules'; @@ -72,8 +70,6 @@ import { generateRowValues } from './utils/row_values'; export class DataController extends modules.Controller { public _dataSource?: DataSourceAdapter | null; - protected isSharedDataSource?: boolean; - protected _items!: ProcessedItem[]; private _cachedProcessedItems!: ProcessedItem[] | null; @@ -126,6 +122,8 @@ export class DataController extends modules.Controller { public rowIndicesChanged!: Callback<[RowIndexCorrection]>; + protected dataSourceController!: DataSourceController; + // TODO public controller public _columnsController!: Controllers['columns']; @@ -140,6 +138,7 @@ export class DataController extends modules.Controller { public init(): void { this._items = []; this._cachedProcessedItems = null; + this.dataSourceController = this.getController('dataSource'); this._columnsController = this.getController('columns'); this._isPaging = false; @@ -643,22 +642,6 @@ export class DataController extends modules.Controller { }); } - protected _getSpecificDataSourceOption(): unknown { - const dataSource = this.option('dataSource'); - - if (Array.isArray(dataSource)) { - return { - store: { - type: 'array', - data: dataSource, - key: this.option('keyExpr'), - }, - }; - } - - return dataSource; - } - /** * @extended: state_storing, virtual_scrolling */ @@ -672,7 +655,9 @@ export class DataController extends modules.Controller { protected _initDataSource(): void { const hadDataSource = !!this._dataSource; - const dataSource = this.recreateDataSource(); + this._disposeDataSource(); + + const dataSource = this.dataSourceController.createDataSource(); this._useSortingGroupingFromColumns = true; this._cachedProcessedItems = null; @@ -686,27 +671,6 @@ export class DataController extends modules.Controller { } } - private recreateDataSource(): DataSource | undefined { - const dataSourceOptions = this._getSpecificDataSourceOption(); - - this._disposeDataSource(); - - if (!dataSourceOptions) { - this.isSharedDataSource = false; - return undefined; - } - - if (dataSourceOptions instanceof DataSourceClass) { - this.isSharedDataSource = true; - return dataSourceOptions as unknown as DataSource; - } - - this.isSharedDataSource = false; - return new DataSourceClass( - extend(true, {}, normalizeDataSourceOptions(dataSourceOptions, {})), - ) as unknown as DataSource; - } - /** * @extended: selection, virtual_scrolling */ @@ -1359,18 +1323,6 @@ export class DataController extends modules.Controller { this.dataSourceChanged.fire(); }; - protected _getDataSourceAdapterProvider(): DataSourceAdapterProvider { - throw new Error('Method not implemented.'); - } - - protected _createDataSourceAdapter(dataSource: DataSource): DataSourceAdapter { - const dataSourceAdapterProvider = this._getDataSourceAdapterProvider(); - const dataSourceAdapter = dataSourceAdapterProvider.create(this.component); - - dataSourceAdapter.init(dataSource); - return dataSourceAdapter; - } - private subscribeToDataSource(dataSourceAdapter: DataSourceAdapter): void { dataSourceAdapter.changed.add(this.dataChangedHandlerProxy); dataSourceAdapter.loadingChanged.add(this.loadingChangedHandler); @@ -1389,29 +1341,17 @@ export class DataController extends modules.Controller { dataSourceAdapter.pushed.remove(this.dataPushedHandlerProxy); } - private setDataSource(dataSource: DataSource | null): void { - const oldDataSource = this._dataSource; - - if (!dataSource && oldDataSource) { - oldDataSource.cancelAll(); - this.unsubscribeFromDataSource(oldDataSource); - oldDataSource.dispose(this.isSharedDataSource); - } - - const dataSourceAdapter = dataSource - ? this._createDataSourceAdapter(dataSource) - : null; + private setDataSource(dataSource: DataSource): void { + const dataSourceAdapter = this.dataSourceController.createAdapter(dataSource); this._dataSource = dataSourceAdapter; - if (dataSourceAdapter) { - this._isLoading = !dataSourceAdapter.isLoaded(); - this._needApplyFilter = true; - this._isAllDataTypesDefined = this._columnsController.isAllDataTypesDefined(); + this._isLoading = !dataSourceAdapter.isLoaded(); + this._needApplyFilter = true; + this._isAllDataTypesDefined = this._columnsController.isAllDataTypesDefined(); - this.changed.add(this.fireDataSourceChanged); - this.subscribeToDataSource(dataSourceAdapter); - } + this.changed.add(this.fireDataSourceChanged); + this.subscribeToDataSource(dataSourceAdapter); } /** @@ -1650,7 +1590,16 @@ export class DataController extends modules.Controller { } protected _disposeDataSource(): void { - this.setDataSource(null); + const oldDataSource = this._dataSource; + + if (oldDataSource) { + // Before unsubscribing: cancelling in-flight loads still notifies this controller. + oldDataSource.cancelAll(); + this.unsubscribeFromDataSource(oldDataSource); + } + + this._dataSource = null; + this.dataSourceController.disposeAdapter(); } public dispose(): void { diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.integration.test.ts new file mode 100644 index 000000000000..cfae192b7095 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.integration.test.ts @@ -0,0 +1,369 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import DataSourceClass from '@js/data/data_source'; +import type { Properties as TreeListProperties } from '@js/ui/tree_list'; +import TreeList from '@js/ui/tree_list'; +import errors from '@js/ui/widget/ui.errors'; +import { + afterTest, + beforeTest, + createDataGrid, + flushAsync, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { Controllers, InternalGrid } from '@ts/grids/grid_core/m_types'; + +import { DataSourceController } from '../data_source_controller'; + +const TREELIST_CONTAINER_ID = 'treeListContainer'; + +const DATA = [ + { id: 1, parentId: 0, value: 'a' }, + { id: 2, parentId: 1, value: 'b' }, +]; + +const OTHER_DATA = [ + { id: 3, parentId: 0, value: 'c' }, +]; + +interface TreeListInstance extends TreeList { + getController: (name: T) => Controllers[T]; +} + +const createTreeList = ( + options: TreeListProperties = {}, +): { $container: dxElementWrapper; instance: TreeListInstance } => { + const $container = $('
') + .attr('id', TREELIST_CONTAINER_ID) + .appendTo(document.body); + + const instance = new TreeList( + $container.get(0) as HTMLDivElement, + { keyExpr: 'id', parentIdExpr: 'parentId', ...options }, + ) as TreeListInstance; + + jest.runAllTimers(); + + return { $container, instance }; +}; + +const disposeTreeList = ($container: dxElementWrapper): void => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (($container as any).dxTreeList('instance') as TreeList | undefined)?.dispose(); + $container.remove(); +}; + +const getControllerNames = (instance: unknown): string[] => Object + .keys((instance as InternalGrid)._controllers); + +describe('dataSource module registration', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('is reachable from DataGrid', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getController('dataSource')).toBeInstanceOf(DataSourceController); + }); + + it('is reachable from TreeList', () => { + const { $container, instance } = createTreeList({ dataSource: DATA }); + + try { + expect(instance.getController('dataSource')).toBeInstanceOf(DataSourceController); + } finally { + disposeTreeList($container); + } + }); + + it('leaves the getDataSource public method on DataController', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getDataSource()).toBe(instance.getController('data').getDataSource()); + }); + + it('sits at the bottom of the controller order', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(getControllerNames(instance)[0]).toBe('dataSource'); + }); +}); + +describe('dataSource controller holds the adapter', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('holds the same adapter object as DataController', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const adapter = instance.getController('data')._dataSource; + + expect(adapter).toBeTruthy(); + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(adapter); + }); + + it('follows the rebuilt adapter when the dataSource option changes', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const dataController = instance.getController('data'); + const firstAdapter = dataSourceController.getAdapter(); + + instance.option('dataSource', OTHER_DATA); + await flushAsync(); + + expect(dataSourceController.getAdapter()).not.toBe(firstAdapter); + expect(dataSourceController.getAdapter()).toBe(dataController._dataSource); + }); + + it('releases the adapter when the dataSource option is cleared', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + instance.option('dataSource', undefined); + await flushAsync(); + + expect(instance.getController('data')._dataSource).toBeNull(); + expect(dataSourceController.hasAdapter()).toBe(false); + expect(dataSourceController.getAdapter()).toBeNull(); + expect(dataSourceController.getDataSource()).toBeNull(); + expect(dataSourceController.store()).toBeUndefined(); + }); + + it('recovers after the dataSource option is set again', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + instance.option('dataSource', undefined); + await flushAsync(); + instance.option('dataSource', OTHER_DATA); + await flushAsync(); + + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(instance.getController('data')._dataSource); + }); + + it('still holds the same adapter after a refresh', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + const refreshed = instance.refresh(); + await flushAsync(); + await refreshed; + + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(instance.getController('data')._dataSource); + }); + + it('releases the adapter on dispose', async () => { + const { $container, instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const dataController = instance.getController('data'); + + instance.dispose(); + $container.remove(); + + expect(dataController._dataSource).toBeNull(); + expect(dataSourceController.hasAdapter()).toBe(false); + }); + + it('holds the adapter in TreeList too', () => { + const { $container, instance } = createTreeList({ dataSource: DATA }); + + try { + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(instance.getController('data')._dataSource); + } finally { + disposeTreeList($container); + } + }); +}); + +describe('dataSource controller reads delegate to the adapter', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('delegates store', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getController('dataSource').store()) + .toBe(instance.getController('data').store()); + }); + + it('delegates key', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getController('dataSource').key()).toBe('id'); + }); + + it('unwraps one hop for getDataSource', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.getDataSource()) + .toBe(instance.getController('data').getDataSource()); + expect(dataSourceController.getDataSource()) + .not.toBe(dataSourceController.getAdapter()); + }); + + it('delegates remoteOperations instead of falling back to an empty object', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const adapter = dataSourceController.getAdapter(); + + expect(dataSourceController.remoteOperations()).toBe(adapter?.remoteOperations()); + }); + + it('delegates getDataIndexGetter', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(typeof instance.getController('dataSource').getDataIndexGetter()).toBe('function'); + }); +}); + +describe('dataSource controller resolves its own component adapter provider', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('builds a DataGrid adapter in DataGrid', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const adapter = instance.getController('dataSource').getAdapter(); + + expect(adapter).toBeTruthy(); + expect('forEachNode' in (adapter as object)).toBe(false); + }); + + it('builds a TreeList adapter in TreeList', () => { + const { $container, instance } = createTreeList({ dataSource: DATA }); + + try { + const adapter = instance.getController('dataSource').getAdapter(); + + expect(adapter).toBeTruthy(); + expect('forEachNode' in (adapter as object)).toBe(true); + } finally { + disposeTreeList($container); + } + }); +}); + +describe('dataSource controller owns the dataSource option reading', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('warns W1011 in DataGrid when keyExpr is combined with a non-array dataSource', async () => { + const log = jest.spyOn(errors, 'log').mockImplementation(() => {}); + + try { + await createDataGrid({ dataSource: { store: { type: 'array', data: DATA } }, keyExpr: 'id' }); + + expect(log).toHaveBeenCalledWith('W1011'); + } finally { + log.mockRestore(); + } + }); + + it('does not warn W1011 in DataGrid for an array dataSource', async () => { + const log = jest.spyOn(errors, 'log').mockImplementation(() => {}); + + try { + await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + expect(log).not.toHaveBeenCalledWith('W1011'); + } finally { + log.mockRestore(); + } + }); + + it('does not warn W1011 in TreeList, where the override does not apply', () => { + const log = jest.spyOn(errors, 'log').mockImplementation(() => {}); + const { $container } = createTreeList({ + dataSource: { store: { type: 'array', data: DATA } }, + keyExpr: 'id', + }); + + try { + expect(log).not.toHaveBeenCalledWith('W1011'); + } finally { + log.mockRestore(); + disposeTreeList($container); + } + }); + + it('builds a DataSource from the array option and keys it by keyExpr', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + expect(instance.getController('dataSource').key()).toBe('id'); + }); +}); + +describe('dataSource controller owns adapter disposal', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('spares a DataSource the caller still owns when the grid is disposed', async () => { + const shared = new DataSourceClass({ store: DATA, key: 'id' }); + const dispose = jest.spyOn(shared, 'dispose'); + const { $container, instance } = await createDataGrid({ dataSource: shared }); + + instance.dispose(); + // afterTest reads the component off #gridContainer, so a disposed one must not linger. + $container.remove(); + + expect(dispose).not.toHaveBeenCalled(); + + dispose.mockRestore(); + shared.dispose(); + }); + + it('destroys a DataSource it built itself when the grid is disposed', async () => { + const { $container, instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + const built = instance.getController('dataSource').getDataSource(); + + if (!built) { + throw new Error('expected the controller to have built a DataSource'); + } + + const dispose = jest.spyOn(built, 'dispose'); + + instance.dispose(); + $container.remove(); + + expect(dispose).toHaveBeenCalledTimes(1); + + dispose.mockRestore(); + }); + + it('spares a shared DataSource when the dataSource option is replaced', async () => { + const shared = new DataSourceClass({ store: DATA, key: 'id' }); + const dispose = jest.spyOn(shared, 'dispose'); + const { instance } = await createDataGrid({ dataSource: shared }); + + instance.option('dataSource', OTHER_DATA); + await flushAsync(); + + expect(dispose).not.toHaveBeenCalled(); + + dispose.mockRestore(); + shared.dispose(); + }); + + it('leaves DataController and the controller agreeing that the adapter is gone', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + instance.option('dataSource', undefined); + await flushAsync(); + + expect(instance.getController('dataSource').hasAdapter()).toBe(false); + expect(instance.getController('dataSource').getAdapter()).toBeNull(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.test.ts new file mode 100644 index 000000000000..d64d70ec46ab --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.test.ts @@ -0,0 +1,493 @@ +import { + describe, + expect, + it, + jest, +} from '@jest/globals'; +import { DataSource as DataSourceClass } from '@js/common/data/data_source/data_source'; +import type Store from '@ts/data/abstract_store'; +import type { StoreKey } from '@ts/data/abstract_store'; +import type { DataSource } from '@ts/data/data_source/data_source'; +import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; +import type { + DataSourceAdapterProvider, RawItemData, RemoteOperationsOptions, +} from '@ts/grids/grid_core/data_source_adapter/types'; +import type { InternalGrid } from '@ts/grids/grid_core/m_types'; + +import { DataSourceController } from '../data_source_controller'; + +interface AdapterStub { + _dataSource: DataSource; + store: jest.Mock<() => Store | undefined>; + key: jest.Mock<() => StoreKey | undefined>; + remoteOperations: jest.Mock<() => RemoteOperationsOptions>; + getDataIndexGetter: jest.Mock<() => (data: RawItemData) => number>; + dispose: jest.Mock<(isShared?: boolean) => void>; + init: jest.Mock<(dataSource: DataSource) => void>; +} + +interface ProviderStub { + nextAdapter: AdapterStub; + create: jest.Mock<(component: InternalGrid) => DataSourceAdapter>; + extend: jest.Mock<() => void>; +} + +const createAdapterStub = (marker: string): AdapterStub => ({ + _dataSource: { marker } as unknown as DataSource, + store: jest.fn(() => ({ marker } as unknown as Store)), + key: jest.fn(() => marker as StoreKey), + remoteOperations: jest.fn(() => ({ filtering: true } as RemoteOperationsOptions)), + getDataIndexGetter: jest.fn(() => (): number => 0), + dispose: jest.fn(), + init: jest.fn(), +}); + +const asAdapter = (stub: AdapterStub): DataSourceAdapter => stub as unknown as DataSourceAdapter; + +class TestDataSourceController extends DataSourceController { + public providerStub!: DataSourceAdapterProvider; + + protected getAdapterProvider(): DataSourceAdapterProvider { + return this.providerStub; + } + + public readSpecificDataSourceOption(): unknown { + return this.getSpecificDataSourceOption(); + } +} + +const createControllerWith = (): { + controller: DataSourceController; + component: InternalGrid; +} => { + const component = { + _optionCache: {}, + _controllers: {}, + option: jest.fn(), + } as unknown as InternalGrid; + + return { controller: new DataSourceController(component), component }; +}; + +const createController = (): DataSourceController => createControllerWith().controller; + +const createProviderStub = (adapter: AdapterStub): ProviderStub => { + const stub: ProviderStub = { + nextAdapter: adapter, + create: jest.fn(() => asAdapter(stub.nextAdapter)), + extend: jest.fn(), + }; + + return stub; +}; + +const asProvider = ( + stub: ProviderStub, +): DataSourceAdapterProvider => stub as unknown as DataSourceAdapterProvider; + +const SOURCE = { marker: 'source' } as unknown as DataSource; + +const withProvider = (adapter: AdapterStub): { + controller: TestDataSourceController; + component: InternalGrid; + provider: ProviderStub; +} => { + const component = { + _optionCache: {}, + _controllers: {}, + option: jest.fn(), + } as unknown as InternalGrid; + const controller = new TestDataSourceController(component); + const provider = createProviderStub(adapter); + + controller.providerStub = asProvider(provider); + + return { controller, component, provider }; +}; + +const withOptions = (options: Record): TestDataSourceController => { + const component = { + _controllers: {}, + option: jest.fn((name?: string) => (name === undefined ? options : options[name])), + } as unknown as InternalGrid; + + return new TestDataSourceController(component); +}; + +// isShared is private and read only by disposal, so that is where it becomes observable. +const flagHandedToAdapter = ( + controller: TestDataSourceController, +): boolean | undefined => { + const probe = createAdapterStub('probe'); + + controller.providerStub = asProvider(createProviderStub(probe)); + controller.createAdapter(SOURCE); + controller.disposeAdapter(); + + return probe.dispose.mock.calls[0]?.[0]; +}; + +const withAdapter = (marker = 'first'): { + controller: TestDataSourceController; + adapter: AdapterStub; + provider: ProviderStub; +} => { + const adapter = createAdapterStub(marker); + const { controller, provider } = withProvider(adapter); + + controller.createAdapter(SOURCE); + + return { controller, adapter, provider }; +}; + +describe('DataSourceController', () => { + describe('with no adapter', () => { + it('has no adapter right after construction, without init()', () => { + const controller = createController(); + + expect(controller.hasAdapter()).toBe(false); + expect(controller.getAdapter()).toBeNull(); + }); + + it('returns null from getDataSource', () => { + expect(createController().getDataSource()).toBeNull(); + }); + + it('returns undefined from store and key', () => { + const controller = createController(); + + expect(controller.store()).toBeUndefined(); + expect(controller.key()).toBeUndefined(); + }); + + it('returns undefined from getDataIndexGetter', () => { + expect(createController().getDataIndexGetter()).toBeUndefined(); + }); + + it('returns an empty object from remoteOperations, so callers can enumerate it', () => { + const controller = createController(); + + expect(controller.remoteOperations()).toEqual({}); + expect(Object.keys(controller.remoteOperations())).toEqual([]); + }); + }); + + describe('with an adapter', () => { + it('reports the adapter as present and hands back the same object', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.hasAdapter()).toBe(true); + expect(controller.getAdapter()).toBe(asAdapter(adapter)); + }); + + it('delegates store to the adapter once per call', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.store()).toBe(adapter.store.mock.results[0]?.value); + expect(adapter.store).toHaveBeenCalledTimes(1); + }); + + it('delegates key to the adapter', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.key()).toBe('first'); + expect(adapter.key).toHaveBeenCalledTimes(1); + }); + + it('returns the adapter remoteOperations object as-is', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.remoteOperations()).toEqual({ filtering: true }); + expect(adapter.remoteOperations).toHaveBeenCalledTimes(1); + }); + + it('delegates getDataIndexGetter to the adapter', () => { + const { controller, adapter } = withAdapter(); + const getter = controller.getDataIndexGetter(); + + expect(getter).toBe(adapter.getDataIndexGetter.mock.results[0]?.value); + expect(adapter.getDataIndexGetter).toHaveBeenCalledTimes(1); + }); + + it('returns the inner DataSource from getDataSource, not the adapter', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.getDataSource()).toBe(adapter._dataSource); + expect(controller.getDataSource()).not.toBe(asAdapter(adapter)); + }); + }); + + describe('replacing the adapter', () => { + it('follows the new adapter after a replacement', () => { + const { controller, adapter: first, provider } = withAdapter(); + const second = createAdapterStub('second'); + + provider.nextAdapter = second; + controller.createAdapter(SOURCE); + + expect(controller.getAdapter()).toBe(asAdapter(second)); + expect(controller.key()).toBe('second'); + expect(controller.getDataSource()).toBe(second._dataSource); + expect(first.key).not.toHaveBeenCalled(); + }); + + it('returns to the absent state after disposeAdapter', () => { + const { controller } = withAdapter(); + + controller.disposeAdapter(); + + expect(controller.hasAdapter()).toBe(false); + expect(controller.getAdapter()).toBeNull(); + expect(controller.getDataSource()).toBeNull(); + expect(controller.store()).toBeUndefined(); + expect(controller.key()).toBeUndefined(); + expect(controller.getDataIndexGetter()).toBeUndefined(); + expect(controller.remoteOperations()).toEqual({}); + }); + }); + + describe('layering', () => { + it('reads no other controller', () => { + const { controller } = withAdapter(); + const getController = jest.spyOn(controller, 'getController'); + + controller.hasAdapter(); + controller.getAdapter(); + controller.getDataSource(); + controller.store(); + controller.key(); + controller.remoteOperations(); + controller.getDataIndexGetter(); + controller.disposeAdapter(); + + expect(getController).not.toHaveBeenCalled(); + }); + }); + + describe('createAdapter', () => { + it('builds the adapter through the provider, passing the component', () => { + const { controller, component, provider } = withProvider(createAdapterStub('built')); + + controller.createAdapter(SOURCE); + + expect(provider.create).toHaveBeenCalledTimes(1); + expect(provider.create).toHaveBeenCalledWith(component); + }); + + it('initialises the adapter with the given data source', () => { + const adapter = createAdapterStub('built'); + + withProvider(adapter).controller.createAdapter(SOURCE); + + expect(adapter.init).toHaveBeenCalledTimes(1); + expect(adapter.init).toHaveBeenCalledWith(SOURCE); + }); + + it('returns the adapter the provider produced', () => { + const adapter = createAdapterStub('built'); + + const result = withProvider(adapter).controller.createAdapter(SOURCE); + + expect(result).toBe(asAdapter(adapter)); + }); + + it('stores the adapter it built', () => { + const adapter = createAdapterStub('built'); + const { controller } = withProvider(adapter); + + controller.createAdapter(SOURCE); + + expect(controller.hasAdapter()).toBe(true); + expect(controller.getAdapter()).toBe(asAdapter(adapter)); + }); + + it('replaces a previously held adapter without disposing it', () => { + const first = createAdapterStub('first'); + const { controller, provider } = withProvider(first); + const second = createAdapterStub('second'); + + controller.createAdapter(SOURCE); + provider.nextAdapter = second; + controller.createAdapter(SOURCE); + + expect(controller.getAdapter()).toBe(asAdapter(second)); + expect(first.dispose).not.toHaveBeenCalled(); + }); + + it('throws on the base class, where no component has supplied a provider', () => { + expect(() => createController().createAdapter(SOURCE)).toThrow('Method not implemented.'); + }); + }); + + describe('getSpecificDataSourceOption', () => { + it('wraps an array option into an array store, keyed by keyExpr', () => { + const data = [{ id: 1 }]; + + const result = withOptions({ dataSource: data, keyExpr: 'id' }) + .readSpecificDataSourceOption(); + + expect(result).toEqual({ store: { type: 'array', data, key: 'id' } }); + }); + + it('keeps the caller array by reference, leaving the copy to createDataSource', () => { + const data = [{ id: 1 }]; + + const result = withOptions({ dataSource: data, keyExpr: 'id' }) + .readSpecificDataSourceOption() as { store: { data: unknown } }; + + expect(result.store.data).toBe(data); + }); + + it('passes a non-array option straight through', () => { + const config = { store: { type: 'odata', url: 'x' } }; + + const result = withOptions({ dataSource: config }).readSpecificDataSourceOption(); + + expect(result).toBe(config); + }); + + it('returns the unset option as-is, so createDataSource can see it is absent', () => { + expect(withOptions({}).readSpecificDataSourceOption()).toBeUndefined(); + }); + + it('does not throw on the base class, unlike getAdapterProvider', () => { + expect(() => withOptions({}).readSpecificDataSourceOption()).not.toThrow(); + }); + }); + + describe('createDataSource', () => { + it('returns undefined when the dataSource option is absent', () => { + const controller = withOptions({}); + + expect(controller.createDataSource()).toBeUndefined(); + }); + + it('leaves the source not-shared when the dataSource option is absent', () => { + const controller = withOptions({}); + + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(false); + }); + + it('builds a DataSource from a plain array', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + expect(controller.createDataSource()).toBeInstanceOf(DataSourceClass); + }); + + it('keys the built DataSource by keyExpr', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + expect(controller.createDataSource()?.key()).toBe('id'); + }); + + it('marks what it built not-shared, so disposal may destroy it', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(false); + }); + + it('builds a DataSource from a store config', () => { + const controller = withOptions({ dataSource: { store: { type: 'array', data: [] } } }); + + expect(controller.createDataSource()).toBeInstanceOf(DataSourceClass); + }); + + it('builds a fresh DataSource on every call', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + expect(controller.createDataSource()).not.toBe(controller.createDataSource()); + }); + + it('hands back the very DataSource the caller passed, without rebuilding it', () => { + const shared = new DataSourceClass({ store: [{ id: 1 }], key: 'id' }); + const controller = withOptions({ dataSource: shared }); + + expect(controller.createDataSource()).toBe(shared); + + shared.dispose(); + }); + + it('marks a caller-owned DataSource shared, so disposal spares it', () => { + const shared = new DataSourceClass({ store: [{ id: 1 }], key: 'id' }); + const controller = withOptions({ dataSource: shared }); + + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(true); + + shared.dispose(); + }); + + it('clears the shared flag when the option moves from a DataSource to an array', () => { + const shared = new DataSourceClass({ store: [{ id: 1 }], key: 'id' }); + const options: Record = { dataSource: shared, keyExpr: 'id' }; + const controller = withOptions(options); + + controller.createDataSource(); + options.dataSource = [{ id: 2 }]; + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(false); + + shared.dispose(); + }); + + it('starts out not-shared, before anything has been created', () => { + expect(flagHandedToAdapter(withOptions({}))).toBe(false); + }); + + it('leaves the held adapter alone — creating a source is not creating an adapter', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + controller.createDataSource(); + + expect(controller.hasAdapter()).toBe(false); + }); + + it('reads no other controller', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + const getController = jest.spyOn(controller, 'getController'); + + controller.createDataSource(); + controller.readSpecificDataSourceOption(); + + expect(getController).not.toHaveBeenCalled(); + }); + }); + + describe('disposeAdapter', () => { + it('disposes the adapter it holds', () => { + const { controller, adapter } = withAdapter(); + + controller.disposeAdapter(); + + expect(adapter.dispose).toHaveBeenCalledTimes(1); + }); + + it('does nothing when there is no adapter to dispose', () => { + expect(() => createController().disposeAdapter()).not.toThrow(); + }); + + it('disposes only once across repeated calls', () => { + const { controller, adapter } = withAdapter(); + + controller.disposeAdapter(); + controller.disposeAdapter(); + + expect(adapter.dispose).toHaveBeenCalledTimes(1); + }); + + it('reads no other controller', () => { + const { controller } = withAdapter(); + const getController = jest.spyOn(controller, 'getController'); + + controller.disposeAdapter(); + + expect(getController).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source/data_source_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source/data_source_controller.ts new file mode 100644 index 000000000000..7a70ce031ba9 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source/data_source_controller.ts @@ -0,0 +1,105 @@ +import { DataSource as DataSourceClass } from '@js/common/data/data_source/data_source'; +import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; +import { extend } from '@js/core/utils/extend'; +import type Store from '@ts/data/abstract_store'; +import type { StoreKey } from '@ts/data/abstract_store'; +import type { DataSource } from '@ts/data/data_source/data_source'; +import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; +import type { + DataSourceAdapterProvider, RawItemData, RemoteOperationsOptions, +} from '@ts/grids/grid_core/data_source_adapter/types'; +import modules from '@ts/grids/grid_core/m_modules'; + +export class DataSourceController extends modules.Controller { + // Absent before the first dataSource assignment and again after a reset. + private adapter: DataSourceAdapter | null = null; + + private isShared = false; + + /** + * @extended: DataGrid's data_source_controller + */ + protected getSpecificDataSourceOption(): unknown { + const dataSource = this.option('dataSource'); + + if (Array.isArray(dataSource)) { + return { + store: { + type: 'array', + data: dataSource, + key: this.option('keyExpr'), + }, + }; + } + + return dataSource; + } + + public createDataSource(): DataSource | undefined { + const dataSourceOptions = this.getSpecificDataSourceOption(); + + if (!dataSourceOptions) { + this.isShared = false; + return undefined; + } + + if (dataSourceOptions instanceof DataSourceClass) { + this.isShared = true; + return dataSourceOptions as unknown as DataSource; + } + + this.isShared = false; + return new DataSourceClass( + extend(true, {}, normalizeDataSourceOptions(dataSourceOptions, {})), + ) as unknown as DataSource; + } + + public getDataSource(): DataSource | null { + return this.adapter?._dataSource ?? null; + } + + /** + * @extended: DataGrid's and TreeList's data_source_controller + */ + protected getAdapterProvider(): DataSourceAdapterProvider { + throw new Error('Method not implemented.'); + } + + public createAdapter(dataSource: DataSource): DataSourceAdapter { + const adapter = this.getAdapterProvider().create(this.component); + + adapter.init(dataSource); + this.adapter = adapter; + + return adapter; + } + + public hasAdapter(): boolean { + return this.adapter !== null; + } + + public getAdapter(): DataSourceAdapter | null { + return this.adapter; + } + + public disposeAdapter(): void { + this.adapter?.dispose(this.isShared); + this.adapter = null; + } + + public store(): Store | undefined { + return this.adapter?.store(); + } + + public key(): StoreKey | undefined { + return this.adapter?.key(); + } + + public remoteOperations(): RemoteOperationsOptions { + return this.adapter?.remoteOperations() ?? {}; + } + + public getDataIndexGetter(): ((data: RawItemData) => number) | undefined { + return this.adapter?.getDataIndexGetter(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 662de74a6193..d056690762f3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -203,6 +203,7 @@ export interface Controllers { columnsResizer: import('./columns_resizing_reordering/m_columns_resizing_reordering').ColumnsResizerViewController; contextMenu: import('./context_menu/m_context_menu').ContextMenuController; data: import('./data_controller/data_controller').DataController; + dataSource: import('./data_source/data_source_controller').DataSourceController; draggingHeader: import('./columns_resizing_reordering/m_columns_resizing_reordering').DraggingHeaderViewController; // todo: export is dataGrid-only controller editing: import('./editing/m_editing').EditingController; diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts index e87ee434b1e8..309f50bd4481 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts @@ -2,11 +2,9 @@ import { equalByValue } from '@js/core/utils/common'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { DataController, dataControllerModule } from '@ts/grids/grid_core/data_controller/data_controller'; -import type { DataSourceAdapterProvider } from '@ts/grids/grid_core/data_source_adapter/types'; import type { RowKey } from '@ts/grids/grid_core/m_types'; import type { DataSourceAdapterTreeList } from '../data_source_adapter/m_data_source_adapter'; -import dataSourceAdapterProvider from '../data_source_adapter/m_data_source_adapter'; import treeListCore from '../m_core'; export class TreeListDataController extends DataController { @@ -16,10 +14,6 @@ export class TreeListDataController extends DataController { return this._dataSource ?? undefined; } - protected _getDataSourceAdapterProvider(): DataSourceAdapterProvider { - return dataSourceAdapterProvider; - } - private _getNodeLevel(node) { let level = -1; while (node.parent) { diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_controller.ts new file mode 100644 index 000000000000..70f64d785c20 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_controller.ts @@ -0,0 +1,9 @@ +import { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; +import type { DataSourceAdapterProvider } from '@ts/grids/grid_core/data_source_adapter/types'; +import dataSourceAdapterProvider from '@ts/grids/tree_list/data_source_adapter/m_data_source_adapter'; + +export class TreeListDataSourceController extends DataSourceController { + protected getAdapterProvider(): DataSourceAdapterProvider { + return dataSourceAdapterProvider; + } +} diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_module.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_module.ts new file mode 100644 index 000000000000..d3b5072248af --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_module.ts @@ -0,0 +1,9 @@ +import treeListCore from '@ts/grids/tree_list/m_core'; + +import { TreeListDataSourceController } from './data_source_controller'; + +treeListCore.registerModule('dataSource', { + controllers: { + dataSource: TreeListDataSourceController, + }, +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts index c933bd272e7e..41ccc3606941 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts @@ -1,5 +1,6 @@ import './module_not_extended/column_headers'; import './m_columns_controller'; +import './data_source/data_source_module'; import './data_controller/m_data_controller'; import './module_not_extended/sorting'; import './rows/m_rows'; @@ -19,6 +20,7 @@ import treeListCore from './m_core'; const TREELIST_CLASS = 'dx-treelist'; treeListCore.registerModulesOrder([ + 'dataSource', 'stateStoring', 'columns', 'selection', diff --git a/packages/devextreme/testing/helpers/gridBaseMocks.js b/packages/devextreme/testing/helpers/gridBaseMocks.js index 05441eea0baa..27c1041868bd 100644 --- a/packages/devextreme/testing/helpers/gridBaseMocks.js +++ b/packages/devextreme/testing/helpers/gridBaseMocks.js @@ -994,11 +994,15 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo _subscribeToEvents(rootElement) { } }; + // The dataSource controller is a leaf that the data controller resolves in init(), + // so it is always included rather than listed by every caller. + const ALWAYS_INCLUDED_MODULES = ['dataSource']; + exports['setup' + nameWidget + 'Modules'] = function(that, moduleNames, options) { const modules = []; $.each(gridCore.modules, function() { - if($.inArray(this.name, moduleNames) !== -1) { + if($.inArray(this.name, moduleNames) !== -1 || $.inArray(this.name, ALWAYS_INCLUDED_MODULES) !== -1) { modules.push(this); } }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js index c2fc39385b72..4b78e7d2de73 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js @@ -383,7 +383,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod dataSource.load(); // act - this.dataController.setDataSource(null); + this.dataController._disposeDataSource(); // assert assert.strictEqual(loadingChangedSpy.callCount, 2, 'loadingChanged call count'); @@ -7746,7 +7746,6 @@ QUnit.module('Filtering', { remoteOperations: { filtering: true } }); - this.dataController.isSharedDataSource = true; this.dataController.setDataSource(this.dataSource); let loadingCount = 0; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js index d83fa3a157c7..eccc30888fb8 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js @@ -31,7 +31,7 @@ const createDataSource = function(options) { setupDataGridModules(dataGridStub, ['data', 'columns']); - const dataSourceAdapter = dataGridStub.dataController._createDataSourceAdapter(dataSource); + const dataSourceAdapter = dataGridStub.dataSourceController.createAdapter(dataSource); const origItems = dataSourceAdapter.items; const processItems = function(items) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js index ee7a4c5f2ac6..5f2f56fb3699 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js @@ -707,7 +707,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod QUnit.test('There are no exceptions on getting node when hasn\'t datasource', function(assert) { // arrange - this.dataController.setDataSource(undefined); + this.dataController._disposeDataSource(); // act, assert assert.equal(this.getNodeByKey(1), undefined, 'no exceptions');