Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TreeViewModel } from '@ts/ui/__tests__/__mock__/model/tree_view';

const CLASSES = {
columnChooser: 'dx-datagrid-column-chooser',
columnChooserList: 'dx-datagrid-column-chooser-list',
popupWrapper: 'dx-popup-wrapper',
};

export class ColumnChooserModel {
constructor(protected readonly root: HTMLElement) {}

private getPopupWrapper(): HTMLElement | null {
return document.body.querySelector(`.${CLASSES.popupWrapper}.${CLASSES.columnChooser}`);
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method queries document.body directly rather than using this.root as the search context. This could cause issues if multiple column choosers exist in tests. Consider scoping the query to this.root or documenting why global document.body search is necessary for popup wrappers (which are typically rendered in a portal outside the grid container).

Suggested change
return document.body.querySelector(`.${CLASSES.popupWrapper}.${CLASSES.columnChooser}`);
const selector = `.${CLASSES.popupWrapper}.${CLASSES.columnChooser}`;
// Prefer searching within the component root to avoid collisions
// when multiple column choosers exist in the same document.
const scopedWrapper = this.root.querySelector(selector) as HTMLElement | null;
if (scopedWrapper) {
return scopedWrapper;
}
// Fallback for cases where the popup is rendered in a portal
// attached directly to document.body.
return document.body.querySelector(selector) as HTMLElement | null;

Copilot uses AI. Check for mistakes.
}

private getOverlay(): HTMLElement | null {
const wrapper = this.getPopupWrapper();
return wrapper?.querySelector('.dx-overlay-content') ?? null;
}

private getTreeView(): TreeViewModel | null {
const overlay = this.getOverlay();
if (!overlay) return null;

const treeViewElement = overlay.querySelector(`.${CLASSES.columnChooserList}`) as HTMLElement;
return treeViewElement ? new TreeViewModel(treeViewElement) : null;
}

public isVisible(): boolean {
return this.getOverlay() !== null;
}

public searchColumn(text: string): void {
const treeView = this.getTreeView();
treeView?.setSearchValue(text);
}

public toggleColumn(columnText: string): void {
const treeView = this.getTreeView();
const checkBox = treeView?.getCheckboxByText(columnText);
checkBox?.toggle();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AIPromptEditorModel } from './ai_prompt_editor';
import { AIHeaderCellModel } from './cell/ai_header_cell';
import { DataCellModel } from './cell/data_cell';
import { HeaderCellModel } from './cell/header_cell';
import { ColumnChooserModel } from './column_chooser';
import { EditFormModel } from './edit_form';
import { DataRowModel } from './row/data_row';

Expand Down Expand Up @@ -134,5 +135,9 @@ export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
return new EditFormModel(this.root.querySelector(`.${this.addWidgetPrefix(SELECTORS.editForm)}`));
}

public getColumnChooser(): ColumnChooserModel {
return new ColumnChooserModel(this.root);
}

public abstract getInstance(): TInstance;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
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 = $('<div>')
.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();
Comment on lines +53 to +56
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup function assumes a DataGrid instance always exists, which may not be true if a test fails early or during grid creation. Add a null check before calling dispose to prevent errors during cleanup. Consider checking if the container exists and has a DataGrid instance before attempting disposal.

Suggested change
const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid;
dataGrid.dispose();
$container.remove();
if ($container.length) {
const hasDxDataGrid = typeof ($container as any).dxDataGrid === 'function';
const dataGrid = hasDxDataGrid
? ($container as any).dxDataGrid('instance') as DataGrid | undefined
: undefined;
if (dataGrid) {
dataGrid.dispose();
}
$container.remove();
}

Copilot uses AI. Check for mistakes.
jest.clearAllMocks();
jest.useRealTimers();
};

describe('Bugs', () => {
beforeEach(beforeTest);
afterEach(afterTest);

describe('T1311329 - DataGrid - Column chooser hides a banded column on using search and recursive selection', () => {
it('should not hide banded column when using search (two levels)', async () => {
const { instance, component } = await createDataGrid({
dataSource: [
{
id: 1,
name: 'Name 1',
value: 10,
phone: 'Banded 1',
email: 'Banded 2',
skype: 'Banded 3',
},
],
columnChooser: {
enabled: true,
search: {
enabled: true,
},
mode: 'select',
selection: {
recursive: true,
selectByClick: true,
allowSelectAll: true,
},
},
columns: [
{ dataField: 'id', caption: 'ID' },
{ dataField: 'name', caption: 'Name' },
{ dataField: 'value', caption: 'Value' },
{
caption: 'Contacts',
columns: [
{
dataField: 'phone',
visible: false,
},
{
dataField: 'email',
},
{
dataField: 'skype',
},
],
},
],
});

let visibleColumnsLevel0 = instance.getVisibleColumns(0);
let visibleColumnsLevel1 = instance.getVisibleColumns(1);

expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined();
expect(visibleColumnsLevel0.find((col) => col.dataField === 'name')).toBeDefined();

instance.showColumnChooser();
jest.runAllTimers();

const columnChooser = component.getColumnChooser();
expect(columnChooser.isVisible()).toBe(true);

columnChooser.searchColumn('n');
jest.runAllTimers();

columnChooser.toggleColumn('Name');
jest.runAllTimers();

visibleColumnsLevel0 = instance.getVisibleColumns(0);
visibleColumnsLevel1 = instance.getVisibleColumns(1);

expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined();
expect(visibleColumnsLevel0.find((col) => col.dataField === 'name')).toBeUndefined();
});

it('should not hide banded column when using search (three levels)', async () => {
const { instance, component } = await createDataGrid({
dataSource: [],
columnChooser: {
enabled: true,
search: {
enabled: true,
},
mode: 'select',
selection: {
recursive: true,
selectByClick: true,
allowSelectAll: true,
},
},
columns: [
{
caption: 'band_level1',
columns: [
{
caption: 'band_level2',
columns: [
{
dataField: 'data1_level3',
visible: false,
},
{
dataField: 'data2_level3',
},
],
},
{
dataField: 'data1_level2',
},
{
dataField: 'data2_level2',
},
],
},
{
dataField: 'data1_level1',
},
],
});

let visibleColumnsLevel0 = instance.getVisibleColumns(0);
let visibleColumnsLevel1 = instance.getVisibleColumns(1);
let visibleColumnsLevel2 = instance.getVisibleColumns(2);

expect(visibleColumnsLevel0.find((col) => col.caption === 'band_level1')).toBeDefined();
expect(visibleColumnsLevel0.find((col) => col.dataField === 'data1_level1')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'data1_level2')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'data2_level2')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.caption === 'band_level2')).toBeDefined();
expect(visibleColumnsLevel2.find((col) => col.dataField === 'data1_level3')).toBeUndefined();
expect(visibleColumnsLevel2.find((col) => col.dataField === 'data2_level3')).toBeDefined();

instance.showColumnChooser();
jest.runAllTimers();

const columnChooser = component.getColumnChooser();
expect(columnChooser.isVisible()).toBe(true);

columnChooser.searchColumn('1');
jest.runAllTimers();

columnChooser.toggleColumn('Data 1 level 1');
Comment on lines +182 to +209
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test relies on DataGrid's auto-generated caption for the column with dataField: 'data1_level1'. While DataGrid does auto-generate captions from dataFields (converting underscores to spaces and capitalizing), explicitly setting a caption would make the test more readable and less fragile to potential changes in the caption generation logic. Consider adding an explicit caption property to make the test's intent clearer.

Copilot uses AI. Check for mistakes.
jest.runAllTimers();

visibleColumnsLevel0 = instance.getVisibleColumns(0);
visibleColumnsLevel1 = instance.getVisibleColumns(1);
visibleColumnsLevel2 = instance.getVisibleColumns(2);

expect(visibleColumnsLevel0.find((col) => col.caption === 'band_level1')).toBeDefined();
expect(visibleColumnsLevel0.find((col) => col.dataField === 'data1_level1')).toBeUndefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'data1_level2')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'data2_level2')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.caption === 'band_level2')).toBeDefined();
expect(visibleColumnsLevel2.find((col) => col.dataField === 'data1_level3')).toBeUndefined();
expect(visibleColumnsLevel2.find((col) => col.dataField === 'data2_level3')).toBeDefined();
});

it('should hide banded column by click', async () => {
const { instance, component } = await createDataGrid({
dataSource: [
{
id: 1,
name: 'Name 1',
value: 10,
phone: 'Banded 1',
email: 'Banded 2',
skype: 'Banded 3',
},
],
columnChooser: {
enabled: true,
search: {
enabled: true,
},
mode: 'select',
selection: {
recursive: true,
selectByClick: true,
allowSelectAll: true,
},
},
columns: [
{ dataField: 'id', caption: 'ID' },
{ dataField: 'name', caption: 'Name' },
{ dataField: 'value', caption: 'Value' },
{
caption: 'Contacts',
columns: [
{
dataField: 'phone',
visible: false,
},
{
dataField: 'email',
},
{
dataField: 'skype',
},
],
},
],
});
let visibleColumnsLevel0 = instance.getVisibleColumns(0);
let visibleColumnsLevel1 = instance.getVisibleColumns(1);

expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined();

instance.showColumnChooser();
jest.runAllTimers();

const columnChooser = component.getColumnChooser();
expect(columnChooser.isVisible()).toBe(true);

columnChooser.toggleColumn('Contacts');
jest.runAllTimers();

visibleColumnsLevel0 = instance.getVisibleColumns(0);
visibleColumnsLevel1 = instance.getVisibleColumns(1);

expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined();
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses .toBeDefined() which passes even when find() returns undefined. This assertion will always pass because find() always returns a value (either an object or undefined), and undefined.toBeDefined() is true. Use .toBeTruthy() or check the result with .not.toBeUndefined() for the intended behavior.

Copilot uses AI. Check for mistakes.
expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined();

columnChooser.toggleColumn('Contacts');
jest.runAllTimers();

visibleColumnsLevel0 = instance.getVisibleColumns(0);
visibleColumnsLevel1 = instance.getVisibleColumns(1);

expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeUndefined();
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous comment, .toBeUndefined() on a find() result will always be false since find() returns undefined (not an object with .toBeUndefined() method). The correct assertion should be .toBe(undefined) or .toBeFalsy() to properly check that the column was not found.

Copilot uses AI. Check for mistakes.
expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeUndefined();
expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeUndefined();
});
});
});
Loading
Loading