Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CardView from 'devextreme-testcafe-models/cardView';
import url from '../../../helpers/getPageUrl';
import { createWidget } from '../../../helpers/createWidget';
import { getCardFieldCaptions } from '../helpers/cardUtils';

fixture`CardView - ColumnChooser.Functional`
.page(url(__dirname, '../../container.html'));
Expand Down Expand Up @@ -184,3 +185,65 @@ test('ColumnChooser should receive and render custom texts', async (t) => {
}).after(async (t) => {
await t.eval(() => location.reload());
});

test('cards should update when column is hidden via column chooser (select mode) (T1324855)', async (t) => {
const cardView = new CardView('#container');

const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
await t.expect(initialCaptions).eql(['A', 'B', 'C']);

await cardView.apiShowColumnChooser();

await t.click(cardView.getColumnChooser().getCheckbox(0));

const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2);
await t.expect(captionsAfterHide).eql(['B', 'C']);

await t.click(cardView.getColumnChooser().getCheckbox(0));

const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3);
await t.expect(captionsAfterShow).eql(['A', 'B', 'C']);
}).before(async () => createWidget('dxCardView', {
dataSource: [
{ a: 1, b: 2, c: 3 },
],
columns: ['a', 'b', 'c'],
columnChooser: {
enabled: true,
mode: 'select',
},
}));

test('cards should update when column is hidden via column chooser (dragAndDrop mode) (T1324855)', async (t) => {
const cardView = new CardView('#container');

const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
await t.expect(initialCaptions).eql(['A', 'B', 'C']);

await cardView.apiShowColumnChooser();

await t.dragToElement(
cardView.getHeaderPanel().getHeaderItem(0).element,
cardView.getColumnChooser().content,
);

const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2);
await t.expect(captionsAfterHide).eql(['B', 'C']);

await t.dragToElement(
cardView.getColumnChooser().getColumn(0),
cardView.getHeaderPanel().element,
);

const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3);
await t.expect(captionsAfterShow).eql(['A', 'B', 'C']);
}).before(async () => createWidget('dxCardView', {
dataSource: [
{ a: 1, b: 2, c: 3 },
],
columns: ['a', 'b', 'c'],
columnChooser: {
enabled: true,
mode: 'dragAndDrop',
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import TreeView from 'devextreme-testcafe-models/treeView';
import { Selector } from 'testcafe';
import url from '../../../helpers/getPageUrl';
import { createWidget } from '../../../helpers/createWidget';
import { getCardFieldCaptions } from '../helpers/cardUtils';
import {
arrayMoveToGap,
dragToColumnChooser,
Expand Down Expand Up @@ -305,3 +306,38 @@ test('drag from columnChooser to headerPanel: when allowReordering: false', asyn
mode: 'dragAndDrop',
},
}));

test('cards should update when columns are reordered (T1324855)', async (t) => {
const cardView = new CardView('#container');

const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
await t.expect(initialCaptions).eql(['A', 'B', 'C']);

const headerPanel = cardView.getHeaderPanel();
const firstHeader = headerPanel.getHeaderItem(0).element;
const secondHeader = headerPanel.getHeaderItem(1).element;

await t.dragToElement(firstHeader, secondHeader, {
destinationOffsetX: -5,
destinationOffsetY: -20,
speed: 0.5,
});

// Wait for headers to update after drag
await t.expect(cardView.getHeaders().getHeaderItemNth(0).element.innerText).notEql('A');

const headerCaptions: string[] = [];
const headersCount = await cardView.getHeaders().getHeaderItemsElements().count;
for (let i = 0; i < headersCount; i += 1) {
headerCaptions.push(await cardView.getHeaders().getHeaderItemNth(i).element.innerText);
}

const cardCaptions = await getCardFieldCaptions(t, cardView, headersCount);
await t.expect(cardCaptions).eql(headerCaptions);
}).before(async () => createWidget('dxCardView', {
dataSource: [
{ a: 1, b: 2, c: 3 },
],
columns: ['a', 'b', 'c'],
allowColumnReordering: true,
}));
26 changes: 26 additions & 0 deletions e2e/testcafe-devextreme/tests/cardView/helpers/cardUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import CardView from 'devextreme-testcafe-models/cardView';

const FIELD_CAPTION_SELECTOR = '.dx-cardview-field-caption';

const getCardFieldCaptions = async (
t: TestController,
cardView: CardView,
expectedCount: number,
cardIndex = 0,
): Promise<string[]> => {
const card = cardView.getCard(cardIndex);
const captionElements = card.element.find(FIELD_CAPTION_SELECTOR);
Copy link
Contributor

@Raushen Raushen Mar 23, 2026

Choose a reason for hiding this comment

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

We need card.getCaptions method


await t.expect(captionElements.count).eql(expectedCount);

const captions: string[] = [];

for (let i = 0; i < expectedCount; i += 1) {
const caption = await captionElements.nth(i).innerText;
captions.push(caption.replace(/:$/, ''));
}

return captions;
};

export { getCardFieldCaptions };
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,88 @@ describe('absence of multiple re-render', () => {
});
});
});

describe('reactivity to column option changes', () => {
const dataSource = [
{ id: 1, name: 'Audi' },
{ id: 2, name: 'BMW' },
];

const columns = [
{ dataField: 'id', caption: 'ID' },
{ dataField: 'name', caption: 'Name' },
];

it('should re-render cards when column caption changes', () => {
const cardTemplate = jest.fn();

const container = document.createElement('div');
const cardView = new CardView(container, {
keyExpr: 'id',
dataSource,
columns,
cardTemplate,
} as CardViewOptions);

cardTemplate.mockClear();
cardView.columnOption('name', 'caption', 'Vehicle');

expect(cardTemplate).toBeCalledTimes(dataSource.length);
});

it('should re-render cards when column format changes', () => {
const cardTemplate = jest.fn();

const container = document.createElement('div');
const cardView = new CardView(container, {
keyExpr: 'id',
dataSource,
columns,
cardTemplate,
} as CardViewOptions);

cardTemplate.mockClear();
cardView.columnOption('id', 'format', 'currency');

expect(cardTemplate).toBeCalledTimes(dataSource.length);
});

it('should re-render cards when column alignment changes', () => {
const cardTemplate = jest.fn();

const container = document.createElement('div');
const cardView = new CardView(container, {
keyExpr: 'id',
dataSource,
columns,
cardTemplate,
} as CardViewOptions);

cardTemplate.mockClear();
cardView.columnOption('name', 'alignment', 'right');

expect(cardTemplate).toBeCalledTimes(dataSource.length);
});

it('should not re-render cards when sort/filter options change without data change', () => {
const cardTemplate = jest.fn();

const container = document.createElement('div');
const cardView = new CardView(container, {
keyExpr: 'id',
dataSource,
columns,
cardTemplate,
sorting: {
mode: 'single',
},
} as CardViewOptions);

cardTemplate.mockClear();
cardView.columnOption('name', 'sortOrder', 'asc');

// Should be called dataSource.length times (once per card for data update),
// not dataSource.length * 2 (which would indicate extra re-render from column metadata).
expect(cardTemplate).toBeCalledTimes(dataSource.length);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,28 @@ import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/co
import { DataController } from '@ts/grids/new/grid_core/data_controller/data_controller';
import { SearchController } from '@ts/grids/new/grid_core/search/index';

import type { CardInfo, Column, FieldInfo } from '../columns_controller/types';
import type {
CardInfo, Column, FieldInfo, VisibleColumn,
} from '../columns_controller/types';
import type { DataObject, Key } from '../data_controller/types';

// NOTE: Column properties that do NOT affect card rendering.
// Changes to these properties trigger data reload via dataController.items,
// so subscribing to them in visibleColumns causes extra re-renders (T1306983, T1309423).
const NON_LAYOUT_COLUMN_KEYS: ReadonlySet<string> = new Set([
'sortOrder',
'sortIndex',
'filterValues',
'filterType',
]);

const getColumnLayoutKey = (column: VisibleColumn): string => {
const entries = Object.entries(column)
.filter(([key]) => !NON_LAYOUT_COLUMN_KEYS.has(key));

return JSON.stringify(entries);
};

export class ItemsController {
private readonly selectedCardKeys = signal<Key[]>([]);

Expand All @@ -19,12 +38,25 @@ export class ItemsController {

public readonly additionalItems = signal<CardInfo[]>([]);

// NOTE: Tracks column properties that affect card rendering.
// Sort/filter properties are excluded — data changes from sort/filter arrive
// separately via dataController.items, so subscribing to them causes extra re-renders.
private readonly visibleColumnsLayout = computed(
() => JSON.stringify(
this.columnsController.visibleColumns.value
.map(getColumnLayoutKey),
),
);

public readonly items = computed(
() => {
// NOTE: We should trigger computed by search options change,
// But all work with these options encapsulated in SearchHighlightTextProcessor
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.searchController.highlightTextOptions.value;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.visibleColumnsLayout.value;

return this.dataController.items.value.map(
(item, itemIndex) => this.createCardInfo(
item,
Expand Down
Loading